scribble-math/dockers/texcmp/texcmp.js
Kevin Barabash 14a58adb90 Migrate to eslint
Summary
We'd like contributors to use the same linter and lint rules that we use
internally.  This diff swaps out eslint for jshint and fixes all lint failures
except for the max-len failures in the test suites.

Test Plan:
- ka-lint src
- make lint
- make test

Reviewers: emily
2015-12-01 10:02:08 -08:00

255 lines
8.7 KiB
JavaScript

/* eslint no-console:0 */
"use strict";
var childProcess = require("child_process");
var fs = require("fs");
var path = require("path");
var Q = require("q"); // To debug, pass Q_DEBUG=1 in the environment
var pngparse = require("pngparse");
var fft = require("ndarray-fft");
var ndarray = require("ndarray-fft/node_modules/ndarray");
var data = require("../../test/screenshotter/ss_data");
// Adapt node functions to Q promises
var readFile = Q.denodeify(fs.readFile);
var writeFile = Q.denodeify(fs.writeFile);
var mkdir = Q.denodeify(fs.mkdir);
var todo;
if (process.argv.length > 2) {
todo = process.argv.slice(2);
} else {
todo = Object.keys(data).filter(function(key) {
return !data[key].nolatex;
});
}
// Dimensions used when we do the FFT-based alignment computation
var alignWidth = 2048; // should be at least twice the width resp. height
var alignHeight = 2048; // of the screenshots, and a power of two.
// Compute required resolution to match test.html. 16px default font,
// scaled to 4em in test.html, and to 1.21em in katex.css. Corresponding
// LaTeX font size is 10pt. There are 72.27pt per inch.
var pxPerEm = 16 * 4 * 1.21;
var pxPerPt = pxPerEm / 10;
var dpi = pxPerPt * 72.27;
var tmpDir = "/tmp/texcmp";
var ssDir = path.normalize(
path.join(__dirname, "..", "..", "test", "screenshotter"));
var imagesDir = path.join(ssDir, "images");
var teximgDir = path.join(ssDir, "tex");
var diffDir = path.join(ssDir, "diff");
var template;
Q.all([
readFile(path.join(ssDir, "test.tex"), "utf-8"),
ensureDir(tmpDir),
ensureDir(teximgDir),
ensureDir(diffDir),
]).spread(function(data) {
template = data;
// dirs have been created, template has been read, now rasterize.
return Q.all(todo.map(processTestCase));
}).done();
// Process a single test case: rasterize, then create diff
function processTestCase(key) {
var itm = data[key];
var tex = "$" + itm.tex + "$";
if (itm.display) {
tex = "\\[" + itm.tex + "\\]";
}
if (itm.pre) {
tex = itm.pre.replace("<br>", "\\\\") + tex;
}
if (itm.post) {
tex = tex + itm.post.replace("<br>", "\\\\");
}
tex = template.replace(/\$.*\$/, tex.replace(/\$/g, "$$$$"));
var texFile = path.join(tmpDir, key + ".tex");
var pdfFile = path.join(tmpDir, key + ".pdf");
var pngFile = path.join(teximgDir, key + "-pdflatex.png");
var browserFile = path.join(imagesDir, key + "-firefox.png");
var diffFile = path.join(diffDir, key + ".png");
// Step 1: write key.tex file
var fftLatex = writeFile(texFile, tex).then(function() {
// Step 2: call "pdflatex key" to create key.pdf
return execFile("pdflatex", [
"-interaction", "nonstopmode", key,
], {cwd: tmpDir});
}).then(function() {
console.log("Typeset " + key);
// Step 3: call "convert ... key.pdf key.png" to create key.png
return execFile("convert", [
"-density", dpi, "-units", "PixelsPerInch", "-flatten",
pdfFile, pngFile,
]);
}).then(function() {
console.log("Rasterized " + key);
// Step 4: apply FFT to that
return readPNG(pngFile).then(fftImage);
});
// Step 5: apply FFT to reference image as well
var fftBrowser = readPNG(browserFile).then(fftImage);
return Q.all([fftBrowser, fftLatex]).spread(function(browser, latex) {
// Now we have the FFT result from both
// Step 6: find alignment which maximizes overlap.
// This uses a FFT-based correlation computation.
var x;
var y;
var real = createMatrix();
var imag = createMatrix();
// Step 6a: (real + i*imag) = latex * conjugate(browser)
for (y = 0; y < alignHeight; ++y) {
for (x = 0; x < alignWidth; ++x) {
var br = browser.real.get(y, x);
var bi = browser.imag.get(y, x);
var lr = latex.real.get(y, x);
var li = latex.imag.get(y, x);
real.set(y, x, br * lr + bi * li);
imag.set(y, x, br * li - bi * lr);
}
}
// Step 6b: (real + i*imag) = inverseFFT(real + i*imag)
fft(-1, real, imag);
// Step 6c: find position where the (squared) absolute value is maximal
var offsetX = 0;
var offsetY = 0;
var maxSquaredNorm = -1; // any result is greater than initial value
for (y = 0; y < alignHeight; ++y) {
for (x = 0; x < alignWidth; ++x) {
var or = real.get(y, x);
var oi = imag.get(y, x);
var squaredNorm = or * or + oi * oi;
if (maxSquaredNorm < squaredNorm) {
maxSquaredNorm = squaredNorm;
offsetX = x;
offsetY = y;
}
}
}
// Step 6d: Treat negative offsets in a non-cyclic way
if (offsetY > (alignHeight / 2)) {
offsetY -= alignHeight;
}
if (offsetX > (alignWidth / 2)) {
offsetX -= alignWidth;
}
console.log("Positioned " + key + ": " + offsetX + ", " + offsetY);
// Step 7: use these offsets to compute difference illustration
var bx = Math.max(offsetX, 0); // browser left padding
var by = Math.max(offsetY, 0); // browser top padding
var lx = Math.max(-offsetX, 0); // latex left padding
var ly = Math.max(-offsetY, 0); // latex top padding
var uw = Math.max(browser.width + bx, latex.width + lx); // union width
var uh = Math.max(browser.height + by, latex.height + ly); // u. height
return execFile("convert", [
// First image: latex rendering, converted to grayscale and padded
"(", pngFile, "-grayscale", "Rec709Luminance",
"-extent", uw + "x" + uh + "-" + lx + "-" + ly,
")",
// Second image: browser screenshot, to grayscale and padded
"(", browserFile, "-grayscale", "Rec709Luminance",
"-extent", uw + "x" + uh + "-" + bx + "-" + by,
")",
// Third image: the per-pixel minimum of the first two images
"(", "-clone", "0-1", "-compose", "darken", "-composite", ")",
// First image is red, second green, third blue channel of result
"-channel", "RGB", "-combine",
"-trim", // remove everything with the same color as the corners
diffFile, // output file name
]);
}).then(function() {
console.log("Compared " + key);
});
}
// Create a directory, but ignore error if the directory already exists.
function ensureDir(dir) {
return mkdir(dir).fail(function(err) {
if (err.code !== "EEXIST") {
throw err;
}
});
}
// Execute a given command, and return a promise to its output.
// Don't denodeify here, since fail branch needs access to stderr.
function execFile(cmd, args, opts) {
var deferred = Q.defer();
childProcess.execFile(cmd, args, opts, function(err, stdout, stderr) {
if (err) {
console.error("Error executing " + cmd + " " + args.join(" "));
console.error(stdout + stderr);
err.stdout = stdout;
err.stderr = stderr;
deferred.reject(err);
} else {
deferred.resolve(stdout);
}
});
return deferred.promise;
}
// Read given file and parse it as a PNG file.
function readPNG(file) {
var deferred = Q.defer();
var onerror = deferred.reject.bind(deferred);
var stream = fs.createReadStream(file);
stream.on("error", onerror);
pngparse.parseStream(stream, function(err, image) {
if (err) {
onerror(err);
return;
}
deferred.resolve(image);
});
return deferred.promise;
}
// Take a parsed image data structure and apply FFT transformation to it
function fftImage(image) {
var real = createMatrix();
var imag = createMatrix();
var idx = 0;
var nchan = image.channels;
var alphachan = 1 - (nchan % 2);
var colorchan = nchan - alphachan;
for (var y = 0; y < image.height; ++y) {
for (var x = 0; x < image.width; ++x) {
var c;
var v = 0;
for (c = 0; c < colorchan; ++c) {
v += 255 - image.data[idx++];
}
for (c = 0; c < alphachan; ++c) {
v += image.data[idx++];
}
real.set(y, x, v);
}
}
fft(1, real, imag);
return {
real: real,
imag: imag,
width: image.width,
height: image.height,
};
}
// Create a new matrix of preconfigured dimensions, initialized to zero
function createMatrix() {
var array = new Float64Array(alignWidth * alignHeight);
return new ndarray(array, [alignWidth, alignHeight]);
}