UNPKG

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
"use strict"; 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;