UNPKG

@pdfme/schemas

Version:

TypeScript base PDF generator and React base UI. Open source, developed by the community, and completely free to use under the MIT license!

498 lines 21.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.filterEndJP = exports.filterStartJP = exports.isFirefox = exports.splitTextToSize = exports.calculateDynamicFontSize = exports.getSplittedLines = exports.getFontKitFont = exports.widthOfTextAtSize = exports.heightOfFontAtSize = exports.getFontDescentInPt = exports.getBrowserVerticalFontAdjustments = void 0; const fontkit = __importStar(require("fontkit")); const common_1 = require("@pdfme/common"); const buffer_1 = require("buffer"); const constants_js_1 = require("./constants.js"); const getBrowserVerticalFontAdjustments = (fontKitFont, fontSize, lineHeight, verticalAlignment) => { const { ascent, descent, unitsPerEm } = fontKitFont; // Fonts have a designed line height that the browser renders when using `line-height: normal` const fontBaseLineHeight = (ascent - descent) / unitsPerEm; // For vertical alignment top // To achieve consistent positioning between browser and PDF, we apply the difference between // the font's actual height and the font size in pixels. // Browsers middle the font within this height, so we only need half of it to apply to the top. // This means the font renders a bit lower in the browser, but achieves PDF alignment const topAdjustment = (fontBaseLineHeight * fontSize - fontSize) / 2; if (verticalAlignment === constants_js_1.VERTICAL_ALIGN_TOP) { return { topAdj: (0, common_1.pt2px)(topAdjustment), bottomAdj: 0 }; } // For vertical alignment bottom and middle // When browsers render text in a non-form element (such as a <div>), some of the text may be // lowered below and outside the containing element if the line height used is less than // the base line-height of the font. // This behaviour does not happen in a <textarea> though, so we need to adjust the positioning // for consistency between editing and viewing to stop text jumping up and down. // This portion of text is half of the difference between the base line height and the used // line height. If using the same or higher line-height than the base font, then line-height // takes over in the browser and this adjustment is not needed. // Unlike the top adjustment - this is only driven by browser behaviour, not PDF alignment. let bottomAdjustment = 0; if (lineHeight < fontBaseLineHeight) { bottomAdjustment = ((fontBaseLineHeight - lineHeight) * fontSize) / 2; } return { topAdj: 0, bottomAdj: (0, common_1.pt2px)(bottomAdjustment) }; }; exports.getBrowserVerticalFontAdjustments = getBrowserVerticalFontAdjustments; const getFontDescentInPt = (fontKitFont, fontSize) => { const { descent, unitsPerEm } = fontKitFont; return (descent / unitsPerEm) * fontSize; }; exports.getFontDescentInPt = getFontDescentInPt; const heightOfFontAtSize = (fontKitFont, fontSize) => { const { ascent, descent, bbox, unitsPerEm } = fontKitFont; const scale = 1000 / unitsPerEm; const yTop = (ascent || bbox.maxY) * scale; const yBottom = (descent || bbox.minY) * scale; let height = yTop - yBottom; height -= Math.abs(descent * scale) || 0; return (height / 1000) * fontSize; }; exports.heightOfFontAtSize = heightOfFontAtSize; const calculateCharacterSpacing = (textContent, textCharacterSpacing) => { return (textContent.length - 1) * textCharacterSpacing; }; const widthOfTextAtSize = (text, fontKitFont, fontSize, characterSpacing) => { const { glyphs } = fontKitFont.layout(text); const scale = 1000 / fontKitFont.unitsPerEm; const standardWidth = glyphs.reduce((totalWidth, glyph) => totalWidth + glyph.advanceWidth * scale, 0) * (fontSize / 1000); return standardWidth + calculateCharacterSpacing(text, characterSpacing); }; exports.widthOfTextAtSize = widthOfTextAtSize; const getFallbackFont = (font) => { const fallbackFontName = (0, common_1.getFallbackFontName)(font); return font[fallbackFontName]; }; const getCacheKey = (fontName) => `getFontKitFont-${fontName}`; const getFontKitFont = async (fontName, font, _cache) => { const fntNm = fontName || (0, common_1.getFallbackFontName)(font); const cacheKey = getCacheKey(fntNm); if (_cache.has(cacheKey)) { return _cache.get(cacheKey); } const currentFont = font[fntNm] || getFallbackFont(font) || (0, common_1.getDefaultFont)()[common_1.DEFAULT_FONT_NAME]; let fontData = currentFont.data; if (typeof fontData === 'string') { fontData = fontData.startsWith('http') ? await fetch(fontData).then((res) => res.arrayBuffer()) : (0, common_1.b64toUint8Array)(fontData); } // Convert fontData to Buffer if it's not already a Buffer let fontDataBuffer; if (fontData instanceof buffer_1.Buffer) { fontDataBuffer = fontData; } else { fontDataBuffer = buffer_1.Buffer.from(fontData); } const fontKitFont = fontkit.create(fontDataBuffer); _cache.set(cacheKey, fontKitFont); return fontKitFont; }; exports.getFontKitFont = getFontKitFont; const isTextExceedingBoxWidth = (text, calcValues) => { const { font, fontSize, characterSpacing, boxWidthInPt } = calcValues; const textWidth = (0, exports.widthOfTextAtSize)(text, font, fontSize, characterSpacing); return textWidth > boxWidthInPt; }; /** * Incrementally checks the current line for its real length * and returns the position where it exceeds the box width. * Returns `null` to indicate if textLine is shorter than the available box. */ const getOverPosition = (textLine, calcValues) => { for (let i = 0; i <= textLine.length; i++) { if (isTextExceedingBoxWidth(textLine.slice(0, i + 1), calcValues)) { return i; } } return null; }; /** * Line breakable chars depend on the language and writing system. * Western writing systems typically use spaces and hyphens as line breakable chars. * Other writing systems often break on word boundaries so the following * does not negatively impact them. * However, this might need to be revisited for broader language support. */ const isLineBreakableChar = (char) => { const lineBreakableChars = [' ', '-', '\u2014', '\u2013']; return lineBreakableChars.includes(char); }; /** * Gets the position of the split. Splits the exceeding line at * the last breakable char prior to it exceeding the bounding box width. */ const getSplitPosition = (textLine, calcValues) => { const overPos = getOverPosition(textLine, calcValues); if (overPos === null) return textLine.length; // input line is shorter than the available space if (textLine[overPos] === ' ') { // if the character immediately beyond the boundary is a space, split return overPos; } let overPosTmp = overPos - 1; while (overPosTmp >= 0) { if (isLineBreakableChar(textLine[overPosTmp])) { return overPosTmp + 1; } overPosTmp--; } // For very long lines with no breakable chars use the original overPos return overPos; }; /** * Recursively splits the line at getSplitPosition. * If there is some leftover, split the rest again in the same manner. */ const getSplittedLines = (textLine, calcValues) => { const splitPos = getSplitPosition(textLine, calcValues); const splittedLine = textLine.substring(0, splitPos).trimEnd(); const rest = textLine.substring(splitPos).trimStart(); if (rest === textLine) { // if we went so small that we want to split on the first char // then end recursion to avoid infinite loop return [textLine]; } if (rest.length === 0) { // end recursion if there is no leftover return [splittedLine]; } return [splittedLine, ...(0, exports.getSplittedLines)(rest, calcValues)]; }; exports.getSplittedLines = getSplittedLines; /** * If using dynamic font size, iteratively increment or decrement the * font size to fit the containing box. * Calculating space usage involves splitting lines where they exceed * the box width based on the proposed size. */ const calculateDynamicFontSize = ({ textSchema, fontKitFont, value, startingFontSize, }) => { const { fontSize: schemaFontSize, dynamicFontSize: dynamicFontSizeSetting, characterSpacing: schemaCharacterSpacing, width: boxWidth, height: boxHeight, lineHeight = constants_js_1.DEFAULT_LINE_HEIGHT, } = textSchema; const fontSize = startingFontSize || schemaFontSize || constants_js_1.DEFAULT_FONT_SIZE; if (!dynamicFontSizeSetting) return fontSize; if (dynamicFontSizeSetting.max < dynamicFontSizeSetting.min) return fontSize; const characterSpacing = schemaCharacterSpacing ?? constants_js_1.DEFAULT_CHARACTER_SPACING; const paragraphs = value.split('\n'); let dynamicFontSize = fontSize; if (dynamicFontSize < dynamicFontSizeSetting.min) { dynamicFontSize = dynamicFontSizeSetting.min; } else if (dynamicFontSize > dynamicFontSizeSetting.max) { dynamicFontSize = dynamicFontSizeSetting.max; } const dynamicFontFit = dynamicFontSizeSetting.fit ?? constants_js_1.DEFAULT_DYNAMIC_FIT; const calculateConstraints = (size) => { let totalWidthInMm = 0; let totalHeightInMm = 0; const boxWidthInPt = (0, common_1.mm2pt)(boxWidth); const firstLineTextHeight = (0, exports.heightOfFontAtSize)(fontKitFont, size); const firstLineHeightInMm = (0, common_1.pt2mm)(firstLineTextHeight * lineHeight); const otherRowHeightInMm = (0, common_1.pt2mm)(size * lineHeight); paragraphs.forEach((paragraph, paraIndex) => { const lines = getSplittedLinesBySegmenter(paragraph, { font: fontKitFont, fontSize: size, characterSpacing, boxWidthInPt, }); lines.forEach((line, lineIndex) => { if (dynamicFontFit === constants_js_1.DYNAMIC_FIT_VERTICAL) { // For vertical fit we want to consider the width of text lines where we detect a split const textWidth = (0, exports.widthOfTextAtSize)(line.replace('\n', ''), fontKitFont, size, characterSpacing); const textWidthInMm = (0, common_1.pt2mm)(textWidth); totalWidthInMm = Math.max(totalWidthInMm, textWidthInMm); } if (paraIndex + lineIndex === 0) { totalHeightInMm += firstLineHeightInMm; } else { totalHeightInMm += otherRowHeightInMm; } }); if (dynamicFontFit === constants_js_1.DYNAMIC_FIT_HORIZONTAL) { // For horizontal fit we want to consider the line's width 'unsplit' const textWidth = (0, exports.widthOfTextAtSize)(paragraph, fontKitFont, size, characterSpacing); const textWidthInMm = (0, common_1.pt2mm)(textWidth); totalWidthInMm = Math.max(totalWidthInMm, textWidthInMm); } }); return { totalWidthInMm, totalHeightInMm }; }; const shouldFontGrowToFit = (totalWidthInMm, totalHeightInMm) => { if (dynamicFontSize >= dynamicFontSizeSetting.max) { return false; } if (dynamicFontFit === constants_js_1.DYNAMIC_FIT_HORIZONTAL) { return totalWidthInMm < boxWidth; } return totalHeightInMm < boxHeight; }; const shouldFontShrinkToFit = (totalWidthInMm, totalHeightInMm) => { if (dynamicFontSize <= dynamicFontSizeSetting.min || dynamicFontSize <= 0) { return false; } return totalWidthInMm > boxWidth || totalHeightInMm > boxHeight; }; let { totalWidthInMm, totalHeightInMm } = calculateConstraints(dynamicFontSize); // Attempt to increase the font size up to desired fit while (shouldFontGrowToFit(totalWidthInMm, totalHeightInMm)) { dynamicFontSize += constants_js_1.FONT_SIZE_ADJUSTMENT; const { totalWidthInMm: newWidth, totalHeightInMm: newHeight } = calculateConstraints(dynamicFontSize); if (newHeight < boxHeight) { totalWidthInMm = newWidth; totalHeightInMm = newHeight; } else { dynamicFontSize -= constants_js_1.FONT_SIZE_ADJUSTMENT; break; } } // Attempt to decrease the font size down to desired fit while (shouldFontShrinkToFit(totalWidthInMm, totalHeightInMm)) { dynamicFontSize -= constants_js_1.FONT_SIZE_ADJUSTMENT; ({ totalWidthInMm, totalHeightInMm } = calculateConstraints(dynamicFontSize)); } return dynamicFontSize; }; exports.calculateDynamicFontSize = calculateDynamicFontSize; const splitTextToSize = (arg) => { const { value, characterSpacing, fontSize, fontKitFont, boxWidthInPt } = arg; const fontWidthCalcValues = { font: fontKitFont, fontSize, characterSpacing, boxWidthInPt, }; let lines = []; value.split(/\r\n|\r|\n|\f|\u000B/g).forEach((line) => { lines = lines.concat(getSplittedLinesBySegmenter(line, fontWidthCalcValues)); }); return lines; }; exports.splitTextToSize = splitTextToSize; const isFirefox = () => navigator.userAgent.toLowerCase().indexOf('firefox') > -1; exports.isFirefox = isFirefox; const getSplittedLinesBySegmenter = (line, calcValues) => { // nothing to process but need to keep this for new lines. if (line.trim() === '') { return ['']; } const { font, fontSize, characterSpacing, boxWidthInPt } = calcValues; const segmenter = new Intl.Segmenter(undefined, { granularity: 'word' }); const iterator = segmenter.segment(line.trimEnd())[Symbol.iterator](); let lines = []; let lineCounter = 0; let currentTextSize = 0; while (true) { const chunk = iterator.next(); if (chunk.done) break; const segment = chunk.value.segment; const textWidth = (0, exports.widthOfTextAtSize)(segment, font, fontSize, characterSpacing); if (currentTextSize + textWidth <= boxWidthInPt) { // the size of boxWidth is large enough to add the segment if (lines[lineCounter]) { lines[lineCounter] += segment; currentTextSize += textWidth + characterSpacing; } else { lines[lineCounter] = segment; currentTextSize = textWidth + characterSpacing; } } else if (segment.trim() === '') { // a segment can be consist of multiple spaces like ' ' // if they overflow the box, treat them as a line break and move to the next line lines[++lineCounter] = ''; currentTextSize = 0; } else if (textWidth <= boxWidthInPt) { // the segment is small enough to be added to the next line lines[++lineCounter] = segment; currentTextSize = textWidth + characterSpacing; } else { // the segment is too large to fit in the boxWidth, we wrap the segment for (const char of segment) { const size = (0, exports.widthOfTextAtSize)(char, font, fontSize, characterSpacing); if (currentTextSize + size <= boxWidthInPt) { if (lines[lineCounter]) { lines[lineCounter] += char; currentTextSize += size + characterSpacing; } else { lines[lineCounter] = char; currentTextSize = size + characterSpacing; } } else { lines[++lineCounter] = char; currentTextSize = size + characterSpacing; } } } } if (lines.some(containsJapanese)) { return adjustEndOfLine((0, exports.filterEndJP)((0, exports.filterStartJP)(lines))); } else { return adjustEndOfLine(lines); } }; // add a newline if the line is the end of the paragraph const adjustEndOfLine = (lines) => { return lines.map((line, index) => { if (index === lines.length - 1) { return line.trimEnd() + '\n'; } else { return line.trimEnd(); } }); }; function containsJapanese(text) { return /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u.test(text); } // // 日本語禁則処理 // // https://www.morisawa.co.jp/blogs/MVP/8760 // // 行頭禁則 const filterStartJP = (lines) => { const filtered = []; let charToAppend = null; lines .slice() .reverse() .forEach((line) => { if (line.trim().length === 0) { filtered.push(''); } else { const charAtStart = line.charAt(0); if (constants_js_1.LINE_START_FORBIDDEN_CHARS.includes(charAtStart)) { if (line.trim().length === 1) { filtered.push(line); charToAppend = null; } else { if (charToAppend) { filtered.push(line.slice(1) + charToAppend); } else { filtered.push(line.slice(1)); } charToAppend = charAtStart; } } else { if (charToAppend) { filtered.push(line + charToAppend); charToAppend = null; } else { filtered.push(line); } } } }); if (charToAppend) { // Handle the case where filtered might be empty const firstItem = filtered.length > 0 ? filtered[0] : ''; // Ensure we're concatenating strings const combinedItem = String(charToAppend) + String(firstItem); return [combinedItem, ...filtered.slice(1)].reverse(); } else { return filtered.reverse(); } }; exports.filterStartJP = filterStartJP; // 行末禁則 const filterEndJP = (lines) => { const filtered = []; let charToPrepend = null; lines.forEach((line) => { if (line.trim().length === 0) { filtered.push(''); } else { const chartAtEnd = line.slice(-1); if (constants_js_1.LINE_END_FORBIDDEN_CHARS.includes(chartAtEnd)) { if (line.trim().length === 1) { filtered.push(line); charToPrepend = null; } else { if (charToPrepend) { filtered.push(charToPrepend + line.slice(0, -1)); } else { filtered.push(line.slice(0, -1)); } charToPrepend = chartAtEnd; } } else { if (charToPrepend) { filtered.push(charToPrepend + line); charToPrepend = null; } else { filtered.push(line); } } } }); if (charToPrepend) { // Handle the case where filtered might be empty const lastItem = filtered.length > 0 ? filtered[filtered.length - 1] : ''; // Ensure we're concatenating strings const combinedItem = String(lastItem) + String(charToPrepend); return [...filtered.slice(0, -1), combinedItem]; } else { return filtered; } }; exports.filterEndJP = filterEndJP; //# sourceMappingURL=helper.js.map