@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!
483 lines (482 loc) • 17.8 kB
JavaScript
import { DEFAULT_FONT_NAME, b64toUint8Array, createDynamicLayoutSplitRange, getDefaultFont, getDynamicLayoutSplitRange, getFallbackFontName, isUrlSafeToFetch, mm2pt, pt2mm, pt2px } from "@pdfme/common";
import * as fontkit from "fontkit";
import { Buffer } from "buffer";
//#region src/text/constants.ts
var ALIGN_LEFT = "left";
var ALIGN_CENTER = "center";
var ALIGN_RIGHT = "right";
var ALIGN_JUSTIFY = "justify";
var DEFAULT_ALIGNMENT = ALIGN_LEFT;
var VERTICAL_ALIGN_MIDDLE = "middle";
var VERTICAL_ALIGN_BOTTOM = "bottom";
var DEFAULT_FONT_COLOR = "#000000";
var PLACEHOLDER_FONT_COLOR = "#A0A0A0";
var TEXT_FORMAT_PLAIN = "plain";
var TEXT_FORMAT_INLINE_MARKDOWN = "inline-markdown";
var DEFAULT_TEXT_FORMAT = TEXT_FORMAT_PLAIN;
var TEXT_OVERFLOW_VISIBLE = "visible";
var TEXT_OVERFLOW_EXPAND = "expand";
var DEFAULT_TEXT_OVERFLOW = TEXT_OVERFLOW_VISIBLE;
var FONT_VARIANT_FALLBACK_SYNTHETIC = "synthetic";
var FONT_VARIANT_FALLBACK_PLAIN = "plain";
var FONT_VARIANT_FALLBACK_ERROR = "error";
var DEFAULT_FONT_VARIANT_FALLBACK = FONT_VARIANT_FALLBACK_SYNTHETIC;
var SYNTHETIC_BOLD_OFFSET_RATIO = .03;
var SYNTHETIC_BOLD_CSS_TEXT_SHADOW = "0.025em 0 0 currentColor";
var CODE_BACKGROUND_COLOR = "#f2f3f5";
var CODE_HORIZONTAL_PADDING = 1.5;
var DYNAMIC_FIT_VERTICAL = "vertical";
var DYNAMIC_FIT_HORIZONTAL = "horizontal";
var DEFAULT_DYNAMIC_FIT = DYNAMIC_FIT_VERTICAL;
var FONT_SIZE_ADJUSTMENT = .25;
var LINE_START_FORBIDDEN_CHARS = [
"、",
"。",
",",
".",
"」",
"』",
")",
"}",
"】",
">",
"≫",
"]",
"・",
"ー",
"―",
"-",
"!",
"!",
"?",
"?",
":",
":",
";",
";",
"/",
"/",
"ゝ",
"々",
"〃",
"ぁ",
"ぃ",
"ぅ",
"ぇ",
"ぉ",
"っ",
"ゃ",
"ゅ",
"ょ",
"ァ",
"ィ",
"ゥ",
"ェ",
"ォ",
"ッ",
"ャ",
"ュ",
"ョ"
];
var LINE_END_FORBIDDEN_CHARS = [
"「",
"『",
"(",
"{",
"【",
"<",
"≪",
"[",
"〘",
"〖",
"〝",
"‘",
"“",
"⦅",
"«"
];
//#endregion
//#region src/box.ts
var createBoxDimension = (value = 0) => ({
top: value,
right: value,
bottom: value,
left: value
});
var normalizeBoxDimension = (value) => ({
top: value?.top ?? 0,
right: value?.right ?? 0,
bottom: value?.bottom ?? 0,
left: value?.left ?? 0
});
var getBoxInsets = (schema) => ({
borderWidth: normalizeBoxDimension(schema.borderWidth),
padding: normalizeBoxDimension(schema.padding)
});
var getBoxVerticalInset = (schema) => {
const { borderWidth, padding } = getBoxInsets(schema);
return borderWidth.top + borderWidth.bottom + padding.top + padding.bottom;
};
var getBoxContentArea = (schema) => {
const { borderWidth, padding } = getBoxInsets(schema);
const leftInset = borderWidth.left + padding.left;
const topInset = borderWidth.top + padding.top;
const rightInset = borderWidth.right + padding.right;
const bottomInset = borderWidth.bottom + padding.bottom;
return {
position: {
x: schema.position.x + leftInset,
y: schema.position.y + topInset
},
width: Math.max(0, schema.width - leftInset - rightInset),
height: Math.max(0, schema.height - topInset - bottomInset),
leftInset,
topInset,
rightInset,
bottomInset
};
};
var hasBoxDimension = (dimension) => {
const resolved = normalizeBoxDimension(dimension);
return resolved.top > 0 || resolved.right > 0 || resolved.bottom > 0 || resolved.left > 0;
};
var getSplitBoxDimension = (dimension, range, totalUnits) => {
const resolved = normalizeBoxDimension(dimension);
const end = range.end ?? totalUnits;
return {
top: range.start === 0 ? resolved.top : 0,
right: resolved.right,
bottom: end >= totalUnits ? resolved.bottom : 0,
left: resolved.left
};
};
var getBoxDimensionPropPanelSchema = (step = 1) => {
const getCommonProp = () => ({
type: "number",
widget: "inputNumber",
props: {
min: 0,
step
},
span: 6
});
return {
top: {
title: "Top",
...getCommonProp()
},
right: {
title: "Right",
...getCommonProp()
},
bottom: {
title: "Bottom",
...getCommonProp()
},
left: {
title: "Left",
...getCommonProp()
}
};
};
//#endregion
//#region src/text/helper.ts
var getBrowserVerticalFontAdjustments = (fontKitFont, fontSize, lineHeight, verticalAlignment) => {
const { ascent, descent, unitsPerEm } = fontKitFont;
const fontBaseLineHeight = (ascent - descent) / unitsPerEm;
const topAdjustment = (fontBaseLineHeight * fontSize - fontSize) / 2;
if (verticalAlignment === "top") return {
topAdj: pt2px(topAdjustment),
bottomAdj: 0
};
let bottomAdjustment = 0;
if (lineHeight < fontBaseLineHeight) bottomAdjustment = (fontBaseLineHeight - lineHeight) * fontSize / 2;
return {
topAdj: 0,
bottomAdj: pt2px(bottomAdjustment)
};
};
var getFontDescentInPt = (fontKitFont, fontSize) => {
const { descent, unitsPerEm } = fontKitFont;
return descent / unitsPerEm * fontSize;
};
var heightOfFontAtSize = (fontKitFont, fontSize) => {
const { ascent, descent, bbox, unitsPerEm } = fontKitFont;
const scale = 1e3 / unitsPerEm;
let height = (ascent || bbox.maxY) * scale - (descent || bbox.minY) * scale;
height -= Math.abs(descent * scale) || 0;
return height / 1e3 * fontSize;
};
var calculateCharacterSpacing = (textContent, textCharacterSpacing) => {
return (textContent.length - 1) * textCharacterSpacing;
};
var TEXT_WIDTH_CACHE_LIMIT = 5e3;
var textWidthCache = /* @__PURE__ */ new WeakMap();
var getTextWidthCache = (fontKitFont) => {
let cache = textWidthCache.get(fontKitFont);
if (!cache) {
cache = /* @__PURE__ */ new Map();
textWidthCache.set(fontKitFont, cache);
}
return cache;
};
var widthOfTextAtSize = (text, fontKitFont, fontSize, characterSpacing) => {
const cache = getTextWidthCache(fontKitFont);
const cacheKey = `${fontSize}\0${characterSpacing}\0${text}`;
const cachedWidth = cache.get(cacheKey);
if (cachedWidth !== void 0) return cachedWidth;
const { glyphs } = fontKitFont.layout(text);
const scale = 1e3 / fontKitFont.unitsPerEm;
const width = glyphs.reduce((totalWidth, glyph) => totalWidth + glyph.advanceWidth * scale, 0) * (fontSize / 1e3) + calculateCharacterSpacing(text, characterSpacing);
if (cache.size >= TEXT_WIDTH_CACHE_LIMIT) cache.clear();
cache.set(cacheKey, width);
return width;
};
var getFallbackFont = (font) => {
return font[getFallbackFontName(font)];
};
var getCacheKey = (fontName) => `getFontKitFont-${fontName}`;
var fetchRemoteFontData = async (url) => {
if (!isUrlSafeToFetch(url)) throw Error("[@pdfme/schemas] Invalid or unsafe URL for font data. Only http: and https: URLs pointing to public hosts are allowed.");
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.arrayBuffer();
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw Error(`[@pdfme/schemas] Failed to fetch remote font data from ${url}. ${reason}`);
}
};
var getFontKitFont = async (fontName, font, _cache) => {
const fntNm = fontName || getFallbackFontName(font);
const cacheKey = getCacheKey(fntNm);
if (_cache.has(cacheKey)) return _cache.get(cacheKey);
let fontData = (font[fntNm] || getFallbackFont(font) || getDefaultFont()[DEFAULT_FONT_NAME]).data;
if (typeof fontData === "string") if (fontData.startsWith("http")) fontData = await fetchRemoteFontData(fontData);
else fontData = b64toUint8Array(fontData);
let fontDataBuffer;
if (fontData instanceof Buffer) fontDataBuffer = fontData;
else fontDataBuffer = Buffer.from(fontData);
const fontKitFont = fontkit.create(fontDataBuffer);
_cache.set(cacheKey, fontKitFont);
return fontKitFont;
};
/**
* 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.
*/
var calculateDynamicFontSize = ({ textSchema, fontKitFont, value, startingFontSize }) => {
const { fontSize: schemaFontSize, dynamicFontSize: dynamicFontSizeSetting, characterSpacing: schemaCharacterSpacing, lineHeight = 1 } = textSchema;
const { width: boxWidth, height: boxHeight } = getBoxContentArea(textSchema);
const fontSize = startingFontSize || schemaFontSize || 13;
if (!dynamicFontSizeSetting) return fontSize;
if (dynamicFontSizeSetting.max < dynamicFontSizeSetting.min) return fontSize;
const characterSpacing = schemaCharacterSpacing ?? 0;
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 ?? "vertical";
const calculateConstraints = (size) => {
let totalWidthInMm = 0;
let totalHeightInMm = 0;
const boxWidthInPt = mm2pt(boxWidth);
const firstLineHeightInMm = pt2mm(heightOfFontAtSize(fontKitFont, size) * lineHeight);
const otherRowHeightInMm = pt2mm(size * lineHeight);
paragraphs.forEach((paragraph, paraIndex) => {
getSplittedLinesBySegmenter(paragraph, {
font: fontKitFont,
fontSize: size,
characterSpacing,
boxWidthInPt
}).forEach((line, lineIndex) => {
if (dynamicFontFit === "vertical") {
const textWidthInMm = pt2mm(widthOfTextAtSize(line.replace("\n", ""), fontKitFont, size, characterSpacing));
totalWidthInMm = Math.max(totalWidthInMm, textWidthInMm);
}
if (paraIndex + lineIndex === 0) totalHeightInMm += firstLineHeightInMm;
else totalHeightInMm += otherRowHeightInMm;
});
if (dynamicFontFit === "horizontal") {
const textWidthInMm = pt2mm(widthOfTextAtSize(paragraph, fontKitFont, size, characterSpacing));
totalWidthInMm = Math.max(totalWidthInMm, textWidthInMm);
}
});
return {
totalWidthInMm,
totalHeightInMm
};
};
const shouldFontGrowToFit = (totalWidthInMm, totalHeightInMm) => {
if (dynamicFontSize >= dynamicFontSizeSetting.max) return false;
if (dynamicFontFit === "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);
while (shouldFontGrowToFit(totalWidthInMm, totalHeightInMm)) {
dynamicFontSize += FONT_SIZE_ADJUSTMENT;
const { totalWidthInMm: newWidth, totalHeightInMm: newHeight } = calculateConstraints(dynamicFontSize);
if (newHeight < boxHeight) {
totalWidthInMm = newWidth;
totalHeightInMm = newHeight;
} else {
dynamicFontSize -= FONT_SIZE_ADJUSTMENT;
break;
}
}
while (shouldFontShrinkToFit(totalWidthInMm, totalHeightInMm)) {
dynamicFontSize -= FONT_SIZE_ADJUSTMENT;
({totalWidthInMm, totalHeightInMm} = calculateConstraints(dynamicFontSize));
}
return dynamicFontSize;
};
var 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|\v/g).forEach((line) => {
lines = lines.concat(getSplittedLinesBySegmenter(line, fontWidthCalcValues));
});
return lines;
};
var isFirefox = () => navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
var wordSegmenter;
var getWordSegmenter = () => {
wordSegmenter ?? (wordSegmenter = new Intl.Segmenter(void 0, { granularity: "word" }));
return wordSegmenter;
};
var getSplittedLinesBySegmenter = (line, calcValues) => {
if (line.trim() === "") return [""];
const { font, fontSize, characterSpacing, boxWidthInPt } = calcValues;
const iterator = getWordSegmenter().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 = widthOfTextAtSize(segment, font, fontSize, characterSpacing);
if (currentTextSize + textWidth <= boxWidthInPt) if (lines[lineCounter]) {
lines[lineCounter] += segment;
currentTextSize += textWidth + characterSpacing;
} else {
lines[lineCounter] = segment;
currentTextSize = textWidth + characterSpacing;
}
else if (segment.trim() === "") {
lines[++lineCounter] = "";
currentTextSize = 0;
} else if (textWidth <= boxWidthInPt) {
lines[++lineCounter] = segment;
currentTextSize = textWidth + characterSpacing;
} else for (const char of segment) {
const size = 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(filterEndJP(filterStartJP(lines)));
else return adjustEndOfLine(lines);
};
var 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);
}
var 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 (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) {
const firstItem = filtered.length > 0 ? filtered[0] : "";
return [String(charToAppend) + String(firstItem), ...filtered.slice(1)].reverse();
} else return filtered.reverse();
};
var filterEndJP = (lines) => {
const filtered = [];
let charToPrepend = null;
lines.forEach((line) => {
if (line.trim().length === 0) filtered.push("");
else {
const chartAtEnd = line.slice(-1);
if (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) {
const lastItem = filtered.length > 0 ? filtered[filtered.length - 1] : "";
const combinedItem = String(lastItem) + String(charToPrepend);
return [...filtered.slice(0, -1), combinedItem];
} else return filtered;
};
//#endregion
//#region src/splitRange.ts
var BUILT_IN_DYNAMIC_LAYOUT_SPLIT_UNITS = {
tableBody: "tableBody",
listItem: "listItem",
textLine: "textLine"
};
var TABLE_BODY_SPLIT_UNIT = BUILT_IN_DYNAMIC_LAYOUT_SPLIT_UNITS.tableBody;
var LIST_ITEM_SPLIT_UNIT = BUILT_IN_DYNAMIC_LAYOUT_SPLIT_UNITS.listItem;
var TEXT_LINE_SPLIT_UNIT = BUILT_IN_DYNAMIC_LAYOUT_SPLIT_UNITS.textLine;
var createTableBodySplitRange = (start, end) => createDynamicLayoutSplitRange(TABLE_BODY_SPLIT_UNIT, start, end);
var createListItemSplitRange = (start, end) => createDynamicLayoutSplitRange(LIST_ITEM_SPLIT_UNIT, start, end);
var createTextLineSplitRange = (start, end) => createDynamicLayoutSplitRange(TEXT_LINE_SPLIT_UNIT, start, end);
var getTableBodyRange = (schema) => getDynamicLayoutSplitRange(schema, TABLE_BODY_SPLIT_UNIT);
var getListItemRange = (schema) => getDynamicLayoutSplitRange(schema, LIST_ITEM_SPLIT_UNIT);
var getTextLineRange = (schema) => getDynamicLayoutSplitRange(schema, TEXT_LINE_SPLIT_UNIT);
//#endregion
export { VERTICAL_ALIGN_MIDDLE as $, ALIGN_RIGHT as A, DYNAMIC_FIT_VERTICAL as B, getBoxInsets as C, ALIGN_CENTER as D, hasBoxDimension as E, DEFAULT_FONT_COLOR as F, PLACEHOLDER_FONT_COLOR as G, FONT_VARIANT_FALLBACK_ERROR as H, DEFAULT_FONT_VARIANT_FALLBACK as I, TEXT_FORMAT_INLINE_MARKDOWN as J, SYNTHETIC_BOLD_CSS_TEXT_SHADOW as K, DEFAULT_TEXT_FORMAT as L, CODE_HORIZONTAL_PADDING as M, DEFAULT_ALIGNMENT as N, ALIGN_JUSTIFY as O, DEFAULT_DYNAMIC_FIT as P, VERTICAL_ALIGN_BOTTOM as Q, DEFAULT_TEXT_OVERFLOW as R, getBoxDimensionPropPanelSchema as S, getSplitBoxDimension as T, FONT_VARIANT_FALLBACK_PLAIN as U, FONT_SIZE_ADJUSTMENT as V, FONT_VARIANT_FALLBACK_SYNTHETIC as W, TEXT_OVERFLOW_EXPAND as X, TEXT_FORMAT_PLAIN as Y, TEXT_OVERFLOW_VISIBLE as Z, isFirefox as _, createListItemSplitRange as a, createBoxDimension as b, getListItemRange as c, calculateDynamicFontSize as d, fetchRemoteFontData as f, heightOfFontAtSize as g, getFontKitFont as h, TEXT_LINE_SPLIT_UNIT as i, CODE_BACKGROUND_COLOR as j, ALIGN_LEFT as k, getTableBodyRange as l, getFontDescentInPt as m, LIST_ITEM_SPLIT_UNIT as n, createTableBodySplitRange as o, getBrowserVerticalFontAdjustments as p, SYNTHETIC_BOLD_OFFSET_RATIO as q, TABLE_BODY_SPLIT_UNIT as r, createTextLineSplitRange as s, BUILT_IN_DYNAMIC_LAYOUT_SPLIT_UNITS as t, getTextLineRange as u, splitTextToSize as v, getBoxVerticalInset as w, getBoxContentArea as x, widthOfTextAtSize as y, DYNAMIC_FIT_HORIZONTAL as z };
//# sourceMappingURL=splitRange-DmVDtmzO.js.map