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.
90 lines
2.5 KiB
JavaScript
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,
|
|
};
|