@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!
233 lines (205 loc) • 7.39 kB
text/typescript
import { PDFFont, PDFDocument } from '@pdfme/pdf-lib';
import type { Font as FontKitFont } from 'fontkit';
import type { TextSchema } from './types.js';
import {
PDFRenderProps,
ColorType,
Font,
getDefaultFont,
getFallbackFontName,
mm2pt,
} from '@pdfme/common';
import {
VERTICAL_ALIGN_TOP,
VERTICAL_ALIGN_MIDDLE,
VERTICAL_ALIGN_BOTTOM,
DEFAULT_FONT_SIZE,
DEFAULT_ALIGNMENT,
DEFAULT_VERTICAL_ALIGNMENT,
DEFAULT_LINE_HEIGHT,
DEFAULT_CHARACTER_SPACING,
DEFAULT_FONT_COLOR,
} from './constants.js';
import {
calculateDynamicFontSize,
heightOfFontAtSize,
getFontDescentInPt,
getFontKitFont,
widthOfTextAtSize,
splitTextToSize,
} from './helper.js';
import { convertForPdfLayoutProps, rotatePoint, hex2PrintingColor } from '../utils.js';
const embedAndGetFontObj = async (arg: {
pdfDoc: PDFDocument;
font: Font;
_cache: Map<PDFDocument, { [key: string]: PDFFont }>;
}) => {
const { pdfDoc, font, _cache } = arg;
if (_cache.has(pdfDoc)) {
return _cache.get(pdfDoc) as { [key: string]: PDFFont };
}
const fontValues = await Promise.all(
Object.values(font).map(async (v) => {
let fontData = v.data;
if (typeof fontData === 'string' && fontData.startsWith('http')) {
fontData = await fetch(fontData).then((res) => res.arrayBuffer());
}
return pdfDoc.embedFont(fontData, {
subset: typeof v.subset === 'undefined' ? true : v.subset,
});
}),
);
const fontObj = Object.keys(font).reduce(
(acc, cur, i) => Object.assign(acc, { [cur]: fontValues[i] }),
{} as { [key: string]: PDFFont },
);
_cache.set(pdfDoc, fontObj);
return fontObj;
};
const getFontProp = ({
value,
fontKitFont,
schema,
colorType,
}: {
value: string;
fontKitFont: FontKitFont;
colorType?: ColorType;
schema: TextSchema;
}) => {
const fontSize = schema.dynamicFontSize
? calculateDynamicFontSize({ textSchema: schema, fontKitFont, value })
: (schema.fontSize ?? DEFAULT_FONT_SIZE);
const color = hex2PrintingColor(schema.fontColor || DEFAULT_FONT_COLOR, colorType);
return {
alignment: schema.alignment ?? DEFAULT_ALIGNMENT,
verticalAlignment: schema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT,
lineHeight: schema.lineHeight ?? DEFAULT_LINE_HEIGHT,
characterSpacing: schema.characterSpacing ?? DEFAULT_CHARACTER_SPACING,
fontSize,
color,
};
};
export const pdfRender = async (arg: PDFRenderProps<TextSchema>) => {
const { value, pdfDoc, pdfLib, page, options, schema, _cache } = arg;
if (!value) return;
const { font = getDefaultFont(), colorType } = options;
const [pdfFontObj, fontKitFont] = await Promise.all([
embedAndGetFontObj({
pdfDoc,
font,
_cache: _cache as unknown as Map<PDFDocument, { [key: string]: PDFFont }>,
}),
getFontKitFont(schema.fontName, font, _cache as Map<string, FontKitFont>),
]);
const fontProp = getFontProp({ value, fontKitFont, schema, colorType });
const { fontSize, color, alignment, verticalAlignment, lineHeight, characterSpacing } = fontProp;
const fontName = (
schema.fontName ? schema.fontName : getFallbackFontName(font)
) as keyof typeof pdfFontObj;
const pdfFontValue = pdfFontObj && pdfFontObj[fontName];
const pageHeight = page.getHeight();
const {
width,
height,
rotate,
position: { x, y },
opacity,
} = convertForPdfLayoutProps({ schema, pageHeight, applyRotateTranslate: false });
if (schema.backgroundColor) {
const color = hex2PrintingColor(schema.backgroundColor, colorType);
page.drawRectangle({ x, y, width, height, rotate, color });
}
const firstLineTextHeight = heightOfFontAtSize(fontKitFont, fontSize);
const descent = getFontDescentInPt(fontKitFont, fontSize);
const halfLineHeightAdjustment = lineHeight === 0 ? 0 : ((lineHeight - 1) * fontSize) / 2;
const lines = splitTextToSize({
value,
characterSpacing,
fontSize,
fontKitFont,
boxWidthInPt: width,
});
// Text lines are rendered from the bottom upwards, we need to adjust the position down
let yOffset = 0;
if (verticalAlignment === VERTICAL_ALIGN_TOP) {
yOffset = firstLineTextHeight + halfLineHeightAdjustment;
} else {
const otherLinesHeight = lineHeight * fontSize * (lines.length - 1);
if (verticalAlignment === VERTICAL_ALIGN_BOTTOM) {
yOffset = height - otherLinesHeight + descent - halfLineHeightAdjustment;
} else if (verticalAlignment === VERTICAL_ALIGN_MIDDLE) {
yOffset =
(height - otherLinesHeight - firstLineTextHeight + descent) / 2 + firstLineTextHeight;
}
}
const pivotPoint = { x: x + width / 2, y: pageHeight - mm2pt(schema.position.y) - height / 2 };
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
lines.forEach((line, rowIndex) => {
const trimmed = line.replace('\n', '');
const textWidth = widthOfTextAtSize(trimmed, fontKitFont, fontSize, characterSpacing);
const textHeight = heightOfFontAtSize(fontKitFont, fontSize);
const rowYOffset = lineHeight * fontSize * rowIndex;
// Adobe Acrobat Reader shows an error if `drawText` is called with an empty text
if (line === '') {
// return; // this also works
line = '\r\n';
}
let xLine = x;
if (alignment === 'center') {
xLine += (width - textWidth) / 2;
} else if (alignment === 'right') {
xLine += width - textWidth;
}
let yLine = pageHeight - mm2pt(schema.position.y) - yOffset - rowYOffset;
// draw strikethrough
if (schema.strikethrough && textWidth > 0) {
const _x = xLine + textWidth + 1;
const _y = yLine + textHeight / 3;
page.drawLine({
start: rotatePoint({ x: xLine, y: _y }, pivotPoint, rotate.angle),
end: rotatePoint({ x: _x, y: _y }, pivotPoint, rotate.angle),
thickness: (1 / 12) * fontSize,
color: color,
opacity,
});
}
// draw underline
if (schema.underline && textWidth > 0) {
const _x = xLine + textWidth + 1;
const _y = yLine - textHeight / 12;
page.drawLine({
start: rotatePoint({ x: xLine, y: _y }, pivotPoint, rotate.angle),
end: rotatePoint({ x: _x, y: _y }, pivotPoint, rotate.angle),
thickness: (1 / 12) * fontSize,
color: color,
opacity,
});
}
if (rotate.angle !== 0) {
// As we draw each line individually from different points, we must translate each lines position
// relative to the UI rotation pivot point. see comments in convertForPdfLayoutProps() for more info.
const rotatedPoint = rotatePoint({ x: xLine, y: yLine }, pivotPoint, rotate.angle);
xLine = rotatedPoint.x;
yLine = rotatedPoint.y;
}
let spacing = characterSpacing;
if (alignment === 'justify' && line.slice(-1) !== '\n') {
// if alignment is `justify` but the end of line is not newline, then adjust the spacing
const iterator = segmenter.segment(trimmed)[Symbol.iterator]();
const len = Array.from(iterator).length;
spacing += (width - textWidth) / len;
}
page.pushOperators(pdfLib.setCharacterSpacing(spacing));
page.drawText(trimmed, {
x: xLine,
y: yLine,
rotate,
size: fontSize,
color,
lineHeight: lineHeight * fontSize,
font: pdfFontValue,
opacity,
});
});
};