Files
shields/lib/text-measurer.js
Paul Melnikow cc9a6db853 Speed up font-width computation in most cases (#1390)
Ref: #1379

This takes a naive approach to font-width computation, the most compute-intensive part of rendering badges.

1. Add the widths of the individual characters.
    - These widths are measured on startup using PDFKit.
2. For each character pair, add a kerning adjustment
    - The difference between the width of each character pair, and the sum of the characters' separate widths.
    - These are computed for each character pair on startup using PDFKit.
3. For a string with characters outside the printable ASCII character set, fall back to PDFKit.

This branch averaged 0.041 ms in `makeBadge`, compared to 0.144 ms on master, a speedup of 73%. That was on a test of 10,000 consecutive requests (using the `benchmark-performance.sh` script, now checked in).

The speedup applies to badges containing exclusively printable ASCII characters. It wouldn't be as dramatic on non-ASCII text. Though, we could add some frequently used non-ASCII characters to the cached set.
2017-12-26 23:57:46 -05:00

90 lines
2.5 KiB
JavaScript

'use strict';
const PDFDocument = require('pdfkit');
class PDFKitTextMeasurer {
constructor(fontPath, fallbackFontPath) {
this.document = new PDFDocument({ size: 'A4', layout: 'landscape' })
.fontSize(11);
try {
this.document.font(fontPath);
} catch(e) {
if (fallbackFontPath) {
console.error(`Text-width computation may be incorrect. Unable to load font at ${fontPath}. Using fallback font ${fallbackFontPath} instead.`);
this.document.font(fallbackFontPath);
} else {
console.error('No fallback font set.');
throw e;
}
}
}
widthOf(str) {
return this.document.widthOfString(str);
}
}
class QuickTextMeasurer {
constructor(fontPath, fallbackFontPath) {
this.baseMeasurer = new PDFKitTextMeasurer(fontPath, fallbackFontPath)
// This will be a Map of characters -> numbers.
this.characterWidths = new Map();
// This will be Map of Maps of characters -> numbers.
this.kerningPairs = new Map();
this._prepare();
}
static printableAsciiCharacters() {
const printableRange = [32, 126];
const length = printableRange[1] - printableRange[0] + 1;
return Array
.from({ length }, (value, i) => printableRange[0] + i)
.map(charCode => String.fromCharCode(charCode));
}
_prepare() {
const charactersToCache = this.constructor.printableAsciiCharacters();
charactersToCache.forEach(char => {
this.characterWidths.set(char, this.baseMeasurer.widthOf(char));
this.kerningPairs.set(char, new Map());
});
charactersToCache.forEach(first => {
charactersToCache.forEach(second => {
const individually = this.characterWidths.get(first) + this.characterWidths.get(second);
const asPair = this.baseMeasurer.widthOf(`${first}${second}`);
const kerningAdjustment = asPair - individually;
this.kerningPairs.get(first).set(second, kerningAdjustment);
});
});
}
widthOf(str) {
const { characterWidths, kerningPairs } = this;
let result = 0;
let previous = null;
for (const character of str) {
if (!characterWidths.has(character)) {
// Bail.
return this.baseMeasurer.widthOf(str);
}
result += characterWidths.get(character);
if (previous !== null) {
result += kerningPairs.get(previous).get(character);
}
previous = character;
}
return result;
}
}
module.exports = {
PDFKitTextMeasurer,
QuickTextMeasurer,
};