大人の事情で公開していなかった部分を含めて、全てみせます。
自分の記録の意味も含めて。
screenshotDiff.js
/*------------------------------------------------------------
大量ページを逐次表示、スクリーンショットをとり
ペアページ同士で比較差分を表示する。
制限事項: 自動アクセス防止ヒューリスティックを採用して
いるサイトは アクセス権限無し(403)を返すため比較差分を
することができない。
node v8.4.0
puppeteer-core v1.8.0
looks-same v4.0.0
@I.Times 2018/9/25
--------------------------------------------------------------*/
/*------------------------------------------------------------
2018/9/24 I.Times Haranaga
puppeteer によるスクリーンショットの工夫
(1) ページ表示後にスクリーンショットを取らず一度リロードを
してからスクリーンショットをとる。
リロード後のほうがページ描画が安定するため。
--------------------------------------------------------------*/
/*------------------------------------------------------------
2018/9/24 I.Times Haranaga
looks-sameには独自修正を加えている。
(1) createDiff実行時に 差分ピクセル数を返すようCallBackを修正
(2) 差分箇所のみに点を描画する画像ファイルを追加作成する。
--------------------------------------------------------------*/
const puppeteer = require('puppeteer-core');
const async = require('async');
const delay = require('delay');
const fs = require('fs');
const che = require('cheerio');
var looksSame = require('looks-same');
var os = require('os');
// 数値を前ゼロ埋めする。
function zeroPadding(num,length){
var ZERO = '0000000000000000';
if(length){
if(length > ZERO.length){
return (ZERO + num).slice(ZERO.length*(-1));
}else if(length > 0){
return (ZERO + num).slice(-length);
}else{
return num;
}
}else{
return (ZERO + num).slice(ZERO.length*(-1));
}
}
const headless = true;
const browserExecutablePath = 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe';
const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36';
//const userAgent = 'BOT/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36';
const WindowWidth = 1920;
const WindowHeight = 1080;
const viewPort = {
width: WindowWidth,
height: WindowHeight,
deviceScaleFactor:0.85,
isMobile:false,
Mobile:false,
hasTouch:false,
isLandscape:false,
};
const NO_SANDBOX = '--no-sandbox';
const _browserOptions = {
headless: headless,
ignoreHTTPSErrors: true,
executablePath: browserExecutablePath,
defaultViewport:viewPort,
args: ['--window-size='+WindowWidth+','+WindowHeight,'--window-position=0,0',NO_SANDBOX],
};
var browseOptions = _browserOptions;
// Async-Awaitの中の例外をキャッチする仕組み
process.on('unhandledRejection', console.dir);
const DIRNAME = __dirname;
(async () => {
// URL一覧を読み込む。
var elements = [];
var elementList = async () => {
var xml_data = await fs.readFileSync("data.xml", "utf-8");
$ = che.load(xml_data);
$("element").each(function(i0, el0) {
var elem = {};
var actualElem = {};
var actual = $(this).children("actual");
actualElem['url'] = actual.children("url").text();
actualElem['networkidle'] = $(actual).children("networkidle").text();
actualElem['delay'] = $(actual).children("delay").text();
elem['actual'] = actualElem;
var targetElem = {};
var target = $(this).children("target");
targetElem['url'] = target.children("url").text()
targetElem['networkidle'] = target.children("networkidle").text()
targetElem['delay'] = target.children("delay").text()
elem['target'] = targetElem;
elem['tolerance'] = $(this).children('tolerance').text();
elements.push(elem);
});
};
elementList();
// スクリーンショットFunction定義:ページを表示,スクリーンショットを取る。
var screenShot = async (element, shotPath) =>{
await page.setUserAgent(userAgent);
await page.goto(element.url, {waitUntil: element.networkidle})
.then( async function(response){
// ReLoadする理由
// ReLoad再描画が時間が掛からず、ScreenShotタイミングを取りやすいため
await page.reload({waitUntil: element.networkidle}).then( async function(response) {
if(element.delay>0){
await delay(element.delay);
}
await page.screenshot( {path: DIRNAME+'\\'+shotPath, fullPage: true});
});
} );
};
// 配列要素を処理するAsync Function定義
var eachProcess = async (element, callback) => {
var index = elements.indexOf( element );
var count = index + 1;
var path = {
imageA: 'imageA\\shotA_'+zeroPadding(count,5)+'.png',
imageB: 'imageB\\shotB_'+zeroPadding(count,5)+'.png'
};
// Actual スクリーンショット
await screenShot(element.actual, path.imageA);
// Target スクリーンショット
await screenShot(element.target, path.imageB);
// Actual<=>Targetの比較
var looksSameOption = {};
looksSameOption.reference = path.imageA;
looksSameOption.current = path.imageB;
looksSameOption.diff = 'diff\\diff_'+zeroPadding(count,5)+'_1.png';
looksSameOption.diff2 = 'diff\\diff_'+zeroPadding(count,5)+'_2.png'; // 独自追加オプション
looksSameOption.highlightColor = '#ff00ff'; //color to highlight the differences
looksSameOption.defaultColor = '#ffffff'; // 独自追加オプション
looksSameOption.strict = true; //strict comparsion
looksSameOption.writeOriginalDiff = true; // 独自追加オプション
looksSame.createDiff(looksSameOption
// このパラメータFunctionは独自追加です。
,function(unmatch){
console.log("No.["+zeroPadding(count,5)+"] UnMatch["+zeroPadding(unmatch)+"]:"+element.actual.url);
// 必ずここのfunctionを一度呼びだすので、ここで forEachSeries のcallbackを呼び出す。
callback();
// このパラメータFunctionは独自追加です。
}, function(err){
if(err){
console.log(err);
throw err;
}
}, function(err){
if(err){
console.log(err);
throw err;
}
} ); // looksSame 終わり
};
// ブラウザを起動する。
var browser = await puppeteer.launch( _browserOptions );
const page = await browser.newPage();
await page.setJavaScriptEnabled(true);
//await page.evaluate('navigator.userAgent');
// キャッシュ無効にする(効果は未確認)
const client = await page.target().createCDPSession();
await client.send( 'Network.setCacheDisabled', { 'cacheDisabled' : true } );
await page.setCacheEnabled( false );
// 配列(elements)の要素をAsync順次処理する。
async.forEachSeries(
// 第一パラメータ:配列
elements,
// 第二パラメータ:要素を処理するAsync Function
(async function(element, callback){
await eachProcess(element, callback);
}),
// 第三パラメータ: 最後に呼び出されるCallBack
async function(err){
if(err) throw err;
await browser.close();
console.log('##browser close');
}
); // async.forEachSeries終わり
})();
data.xml(例)
<element>
<actual>
<url>https://www.naro.affrc.go.jp/nivfs/index.html</url>
<networkidle>networkidle0</networkidle>
<delay>0</delay>
</actual>
<target>
<url>https://www.naro.affrc.go.jp/nivfs/index.html</url>
<networkidle>networkidle0</networkidle>
<delay>0</delay>
</target>
<tolerance>0</tolerance>
</element>
<element>
<actual>
<url>https://www.paxcompy.co.jp/</url>
<networkidle>networkidle0</networkidle>
<delay>0</delay>
</actual>
<target>
<url>https://www.paxcompy.co.jp/</url>
<networkidle>networkidle0</networkidle>
<delay>0</delay>
</target>
<tolerance>0</tolerance>
</element>
<element>
<actual>
<url>http://www.machidukuri-nagano.jp/</url>
<networkidle>networkidle0</networkidle>
<delay>0</delay>
</actual>
<target>
<url>http://www.machidukuri-nagano.jp/</url>
<networkidle>networkidle0</networkidle>
<delay>0</delay>
</target>
<tolerance>0</tolerance>
</element>
node_modules\looks-same\index.js
'use strict';
const _ = require('lodash');
const parseColor = require('parse-color');
const colorDiff = require('color-diff');
const png = require('./lib/png');
const areColorsSame = require('./lib/same-colors');
const AntialiasingComparator = require('./lib/antialiasing-comparator');
const IgnoreCaretComparator = require('./lib/ignore-caret-comparator');
const utils = require('./lib/utils');
const readPair = utils.readPair;
const getDiffPixelsCoords = utils.getDiffPixelsCoords;
const JND = 2.3; // Just noticeable difference if ciede2000 >= JND then colors difference is noticeable by human eye
const getDiffArea = (diffPixelsCoords) => {
const xs = [];
const ys = [];
diffPixelsCoords.forEach((coords) => {
xs.push(coords[0]);
ys.push(coords[1]);
});
const top = Math.min.apply(Math, ys);
const bottom = Math.max.apply(Math, ys);
const left = Math.min.apply(Math, xs);
const right = Math.max.apply(Math, xs);
const width = (right - left) + 1;
const height = (bottom - top) + 1;
return {left, top, width, height};
};
const makeAntialiasingComparator = (comparator, png1, png2, opts) => {
const antialiasingComparator = new AntialiasingComparator(comparator, png1, png2, opts);
return (data) => antialiasingComparator.compare(data);
};
const makeNoCaretColorComparator = (comparator, pixelRatio) => {
const caretComparator = new IgnoreCaretComparator(comparator, pixelRatio);
return (data) => caretComparator.compare(data);
};
function makeCIEDE2000Comparator(tolerance) {
return function doColorsLookSame(data) {
if (areColorsSame(data)) {
return true;
}
/*jshint camelcase:false*/
const lab1 = colorDiff.rgb_to_lab(data.color1);
const lab2 = colorDiff.rgb_to_lab(data.color2);
return colorDiff.diff(lab1, lab2) < tolerance;
};
}
const createComparator = (png1, png2, opts) => {
let comparator = opts.strict ? areColorsSame : makeCIEDE2000Comparator(opts.tolerance);
if (opts.ignoreAntialiasing) {
comparator = makeAntialiasingComparator(comparator, png1, png2, opts);
}
if (opts.ignoreCaret) {
comparator = makeNoCaretColorComparator(comparator, opts.pixelRatio);
}
return comparator;
};
const iterateRect = (width, height, callback, endCallback) => {
const processRow = (y) => {
setImmediate(() => {
for (let x = 0; x < width; x++) {
callback(x, y);
}
y++;
if (y < height) {
processRow(y);
} else {
endCallback();
}
});
};
processRow(0);
};
const buildDiffImage = (png1, png2, options, callback) => {
const width = Math.max(png1.width, png2.width);
const height = Math.max(png1.height, png2.height);
const minWidth = Math.min(png1.width, png2.width);
const minHeight = Math.min(png1.height, png2.height);
const highlightColor = options.highlightColor;
const result = png.empty(width, height);
// ###### ここを変えた ########
// -- add start ---
const result2 = (options.writeOriginalDiff)? png.empty(width, height): null;
var unmatch = 0;
// -- add end ---
iterateRect(width, height, (x, y) => {
if (x >= minWidth || y >= minHeight) {
result.setPixel(x, y, highlightColor);
// ###### ここを変えた ########
// -- add start ---
unmatch += 1;
// -- add end ---
return;
}
const color1 = png1.getPixel(x, y);
const color2 = png2.getPixel(x, y);
if (!options.comparator({color1, color2})) {
result.setPixel(x, y, highlightColor);
// ###### ここを変えた ########
// -- add start ---
unmatch += 1;
if(options.writeOriginalDiff){
result2.setPixel(x, y, options.highlightColor);
}
// -- add end ---
} else {
result.setPixel(x, y, color1);
// ###### ここを変えた ########
// add start
if(options.writeOriginalDiff){
result2.setPixel(x, y, options.defaultColor);
}
// add end
}
// ###### ここを変えた ########
//}, () => callback(result));
}, () => callback(result, result2, unmatch));
};
const parseColorString = (str) => {
const parsed = parseColor(str);
return {
R: parsed.rgb[0],
G: parsed.rgb[1],
B: parsed.rgb[2]
};
};
const getToleranceFromOpts = (opts) => {
if (!_.hasIn(opts, 'tolerance')) {
return JND;
}
if (opts.strict) {
throw new TypeError('Unable to use "strict" and "tolerance" options together');
}
return opts.tolerance;
};
const prepareOpts = (opts) => {
opts.tolerance = getToleranceFromOpts(opts);
_.defaults(opts, {
ignoreAntialiasing: true,
antialiasingTolerance: 0
});
};
module.exports = exports = function looksSame(reference, image, opts, callback) {
if (!callback) {
callback = opts;
opts = {};
}
prepareOpts(opts);
readPair(reference, image, (error, pair) => {
if (error) {
return callback(error);
}
const first = pair.first;
const second = pair.second;
if (first.width !== second.width || first.height !== second.height) {
return process.nextTick(() => callback(null, false));
}
const comparator = createComparator(first, second, opts);
getDiffPixelsCoords(first, second, comparator, {stopOnFirstFail: true}, (result) => {
callback(null, result.length === 0);
});
});
};
exports.getDiffArea = function(reference, image, opts, callback) {
if (!callback) {
callback = opts;
opts = {};
}
prepareOpts(opts);
readPair(reference, image, (error, pair) => {
if (error) {
return callback(error);
}
const first = pair.first;
const second = pair.second;
if (first.width !== second.width || first.height !== second.height) {
return process.nextTick(() => callback(null, {
width: Math.max(first.width, second.width),
height: Math.max(first.height, second.height),
top: 0,
left: 0
}));
}
const comparator = createComparator(first, second, opts);
getDiffPixelsCoords(first, second, comparator, (result) => {
if (!result.length) {
return callback(null, null);
}
callback(null, getDiffArea(result));
});
});
};
//### ここ変えた!
//exports.createDiff = function saveDiff(opts, callback) {
exports.createDiff = function saveDiff(opts, callback, callback2, callback3) {
const tolerance = getToleranceFromOpts(opts);
readPair(opts.reference, opts.current, (error, result) => {
if (error) {
return callback(error);
}
const diffOptions = {
highlightColor: parseColorString(opts.highlightColor),
// ### 下記1行追加
defaultColor : parseColorString(opts.defaultColor),
writeOriginalDiff: opts.writeOriginalDiff,
comparator: opts.strict ? areColorsSame : makeCIEDE2000Comparator(tolerance)
};
// ### ここ変えた!!
//buildDiffImage(result.first, result.second, diffOptions, (result) => {
buildDiffImage(result.first, result.second, diffOptions, (result, result2, unmatch) => {
// ### ここ変えた!!
//if (opts.diff === undefined) {
// result.createBuffer(callback);
//} else {
// result.save(opts.diff, callback);
//}
if (opts.diff === undefined) {
result.createBuffer(callback2);
} else {
result.save(opts.diff, callback2);
if(opts.diff2 && result2){
result2.save(opts.diff2, callback3);
}
callback(unmatch);
}
});
});
};
exports.colors = (color1, color2, opts) => {
opts = opts || {};
if (opts.tolerance === undefined) {
opts.tolerance = JND;
}
const comparator = makeCIEDE2000Comparator(opts.tolerance);
return comparator({color1, color2});
};