ultimate-text-to-image
Version:
Generate UTF8 texts into image with auto line break for all international language, including Chinese, Japanese, Korean, etc..
250 lines (249 loc) • 12.4 kB
JavaScript
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Measurable = void 0;
const canvas_1 = require("canvas");
const UnicodeLineBreak_1 = require("./UnicodeLineBreak");
const utils_1 = require("./utils");
class Measurable {
constructor() {
this.caches = new Map();
}
clearCache() {
this.caches.clear();
}
getMeasuredParagraph(options) {
const canvas = (0, canvas_1.createCanvas)(100, 100);
const ctx = canvas.getContext("2d");
return this.testBestMeasuredParagraph(Object.assign({ ctx }, options));
}
// the logic below may cause the last checking run one more time (but it's ok since things are cached)
testBestMeasuredParagraph(options) {
const { maxWidth, maxHeight, maxFontSize, minFontSize, fontSize } = options, otherOptions = __rest(options, ["maxWidth", "maxHeight", "maxFontSize", "minFontSize", "fontSize"]);
const measuredParagraph = this.testMeasuredParagraph(Object.assign(Object.assign({}, otherOptions), { maxWidth, fontSize }));
const currentHeight = options.useGlyphPadding ? measuredParagraph.boundingHeight : measuredParagraph.height;
const currentWidth = options.useGlyphPadding ? measuredParagraph.boundingWidth : measuredParagraph.width;
// if height is within range
if (currentHeight <= maxHeight && currentWidth <= maxWidth) {
// we still can try to do searching
if (options.maxFontSize > measuredParagraph.fontSize) {
const newFontSize = Math.ceil((options.fontSize + options.maxFontSize) / 2);
return this.testBestMeasuredParagraph(Object.assign(Object.assign({}, options), { fontSize: newFontSize, minFontSize: measuredParagraph.fontSize }));
}
else {
return measuredParagraph;
}
}
else {
// if we have smaller available font size
if (options.minFontSize < measuredParagraph.fontSize) {
// we try an Log(N) guess
const newFontSize = Math.floor((options.fontSize + options.minFontSize) / 2);
return this.testBestMeasuredParagraph(Object.assign(Object.assign({}, options), { fontSize: newFontSize, maxFontSize: measuredParagraph.fontSize - 1 }));
}
else {
return measuredParagraph;
}
}
}
// give information of all the lines
testMeasuredParagraph(options) {
const { ctx, text, noAutoWrap, fontFamily, fontStyle, fontWeight, fontSize, lineHeight, lineHeightMultiplier, autoWrapLineHeight, autoWrapLineHeightMultiplier, useGlyphPadding, } = options;
const measuredWords = this.testMeasureWords({ ctx, text, fontStyle, fontWeight, fontSize, fontFamily });
const maxWidth = options.maxWidth;
// prepare settings
const finalLineHeight = Math.round(typeof lineHeight === "number" ? lineHeight :
(typeof lineHeightMultiplier === "number" ? fontSize * lineHeightMultiplier : fontSize));
const finalAutoWrapLineHeight = Math.round(typeof autoWrapLineHeight === "number" ? autoWrapLineHeight :
(typeof autoWrapLineHeightMultiplier === "number" ? fontSize * autoWrapLineHeightMultiplier : finalLineHeight));
// find space width first
const measuredSpace = this.testMeasuredWord(Object.assign(Object.assign({}, options), { text: " " }));
const spaceWidth = measuredSpace.width;
// prepare canvas
let measuredLine = {
text: "",
width: 0,
paddingTop: -fontSize,
paddingBottom: 0,
paddingLeft: 0,
paddingRight: 0,
nextLineHeight: 0,
measuredWords: [],
};
const measuredParagraph = {
text,
width: 0,
height: 0,
fontSize,
fontFamily,
fontStyle,
fontWeight,
spaceWidth,
boundingHeight: 0,
boundingWidth: 0,
paddingTop: 0,
paddingBottom: 0,
paddingLeft: 0,
paddingRight: 0,
measuredLines: [],
};
let lastMeasuredWord;
for (const measuredWord of measuredWords) {
// we get the last word spacing information
let lastWordTotalSpaceWidth = 0;
let lastWordSpaceCount = 0;
if (lastMeasuredWord && !lastMeasuredWord.hasLineBreak) {
lastWordTotalSpaceWidth = lastMeasuredWord.endingSpaceCount * spaceWidth;
lastWordSpaceCount = lastMeasuredWord.endingSpaceCount;
}
// choose which width for calculation
let currentWidth = measuredLine.width + lastWordTotalSpaceWidth + measuredWord.width;
if (useGlyphPadding) {
currentWidth += measuredWord.paddingLeft + measuredWord.paddingRight;
}
// if we have auto Wrap & make sure each line within max width
if (!noAutoWrap && currentWidth > maxWidth) {
// we go into another line the line contains something already
if (measuredLine.text) {
measuredLine.nextLineHeight = finalAutoWrapLineHeight;
measuredParagraph.measuredLines.push(measuredLine);
}
// create new line
measuredLine = {
text: measuredWord.text,
width: measuredWord.width,
paddingTop: measuredWord.paddingTop,
paddingBottom: measuredWord.paddingBottom,
paddingLeft: measuredWord.paddingLeft,
paddingRight: measuredWord.paddingRight,
nextLineHeight: 0,
measuredWords: [],
};
}
else {
// add the word
measuredLine.text += (" ".repeat(lastWordSpaceCount) + measuredWord.text);
measuredLine.paddingTop = Math.max(measuredLine.paddingTop, measuredWord.paddingTop);
measuredLine.paddingBottom = Math.max(measuredLine.paddingBottom, measuredWord.paddingBottom);
if (measuredLine.width === 0) {
measuredLine.paddingLeft = measuredWord.paddingLeft;
}
measuredLine.paddingRight = measuredWord.paddingRight;
measuredLine.width = measuredLine.width + lastWordTotalSpaceWidth + measuredWord.width;
measuredLine.measuredWords.push(measuredWord);
}
/// if it's not last word, do some further processing
if (!measuredWord.isLastWord) {
if (measuredWord.hasLineBreak) {
measuredLine.nextLineHeight = finalLineHeight;
measuredParagraph.measuredLines.push(measuredLine);
measuredLine = {
text: "",
width: 0,
paddingTop: -fontSize,
paddingBottom: 0,
paddingLeft: 0,
paddingRight: 0,
nextLineHeight: 0,
measuredWords: [],
};
}
}
lastMeasuredWord = measuredWord;
}
// if we current measuredLine has width, add it
if (measuredLine.width) {
measuredParagraph.measuredLines.push(measuredLine);
}
// make sure we have lines
// compute some final params
const totalLines = measuredParagraph.measuredLines.length;
if (totalLines) {
measuredParagraph.width = measuredParagraph.measuredLines
.reduce((a, b) => Math.max(a, b.width), 0);
measuredParagraph.height = measuredParagraph.measuredLines
.reduce((a, b) => a + b.nextLineHeight, measuredParagraph.fontSize);
const paddingTop = measuredParagraph.measuredLines[0].paddingTop;
const paddingBottom = measuredParagraph.measuredLines[totalLines - 1].paddingBottom;
measuredParagraph.paddingTop = paddingTop;
measuredParagraph.paddingBottom = paddingBottom;
measuredParagraph.boundingHeight = measuredParagraph.height + paddingTop + paddingBottom;
measuredParagraph.boundingWidth = measuredParagraph.measuredLines.reduce((a, b) => Math.max(a, b.width + b.paddingLeft + b.paddingRight), 0);
measuredParagraph.paddingLeft = measuredParagraph.measuredLines.reduce((a, b) => Math.max(a, b.paddingLeft), -fontSize);
measuredParagraph.paddingRight = measuredParagraph.measuredLines.reduce((a, b) => Math.max(a, b.paddingRight), -fontSize);
}
return measuredParagraph;
}
// give information for all the words
testMeasureWords(options) {
const measuredWords = [];
const unicodeLineBreak = new UnicodeLineBreak_1.UnicodeLineBreak(options.text);
for (const item of unicodeLineBreak) {
const word = item.word;
const trimmedWord = word.trimRight();
const measuredWord = this.testMeasuredWord(Object.assign(Object.assign({}, options), { text: trimmedWord }));
measuredWords.push({
text: trimmedWord,
width: measuredWord.width,
paddingTop: measuredWord.paddingTop,
paddingBottom: measuredWord.paddingBottom,
paddingLeft: measuredWord.paddingLeft,
paddingRight: measuredWord.paddingRight,
endingSpaceCount: word.length - trimmedWord.length,
isLastWord: item.isLastWord,
hasLineBreak: item.hasLineBreak,
});
}
return measuredWords;
}
testMeasuredWord(options) {
const { ctx, text, fontStyle, fontWeight, fontSize, fontFamily } = options;
const fontString = (0, utils_1.getFontString)({ fontStyle, fontWeight, fontSize, fontFamily });
ctx.font = fontString;
// create font family map if not exist
if (!this.caches.has(fontString)) {
this.caches.set(fontString, new Map());
}
// create font size map if not exist
const fontFamilyMap = this.caches.get(fontString);
if (!fontFamilyMap.has(fontSize)) {
fontFamilyMap.set(fontSize, new Map());
}
// calculate the word width
const fontSizeMap = fontFamilyMap.get(fontSize);
if (!fontSizeMap.has(text)) {
const measureText = ctx.measureText(text);
let paddingLeft = measureText.actualBoundingBoxLeft;
let paddingRight = measureText.actualBoundingBoxRight - measureText.width;
// Special Handling: if this is reversed type of language
if (measureText.width > 0 &&
measureText.actualBoundingBoxLeft / measureText.width > 0.8 &&
measureText.actualBoundingBoxRight / measureText.width < 0.2) {
paddingLeft = measureText.actualBoundingBoxLeft - measureText.width;
paddingRight = measureText.actualBoundingBoxRight;
}
// console.log(text, measureText);
// console.log("measure", `${fontStyle}, left: ${paddingLeft}, right: ${paddingRight}, ${text}`);
fontSizeMap.set(text, {
text,
width: measureText.width,
paddingTop: measureText.actualBoundingBoxAscent - fontSize,
paddingBottom: measureText.actualBoundingBoxDescent,
paddingLeft,
paddingRight,
});
}
return fontSizeMap.get(text);
}
}
exports.Measurable = Measurable;
;