@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!
384 lines • 19.5 kB
JavaScript
import { readFileSync } from 'fs';
import * as path from 'path';
import { getDefaultFont } from '@pdfme/common';
import { calculateDynamicFontSize, getBrowserVerticalFontAdjustments, getFontDescentInPt, getFontKitFont, getSplittedLines, filterStartJP, filterEndJP, } from '../src/text/helper.js';
import { LINE_START_FORBIDDEN_CHARS, LINE_END_FORBIDDEN_CHARS } from '../src/text/constants.js';
const sansData = readFileSync(path.join(__dirname, `/assets/fonts/SauceHanSansJP.ttf`));
const serifData = readFileSync(path.join(__dirname, `/assets/fonts/SauceHanSerifJP.ttf`));
const getSampleFont = () => ({
SauceHanSansJP: { fallback: true, data: sansData },
SauceHanSerifJP: { data: serifData },
});
const getTextSchema = () => {
const textSchema = {
name: 'test',
type: 'text',
content: 'test',
position: { x: 0, y: 0 },
width: 50,
height: 20,
alignment: 'left',
verticalAlignment: 'top',
fontColor: '#000000',
backgroundColor: '#ffffff',
lineHeight: 1,
characterSpacing: 1,
fontSize: 14,
};
return textSchema;
};
describe('getSplitPosition test with mocked font width calculations', () => {
/**
* To simplify these tests we mock the widthOfTextAtSize function to return
* the length of the text in number of characters.
* Therefore, setting the boxWidthInPt to 5 should result in a split after 5 characters.
*/
let widthOfTextAtSizeSpy;
beforeAll(() => {
// @ts-ignore
widthOfTextAtSizeSpy = jest.spyOn(require('../src/text/helper'), 'widthOfTextAtSize');
widthOfTextAtSizeSpy.mockImplementation((text) => {
return text.length;
});
});
afterAll(() => {
widthOfTextAtSizeSpy.mockRestore();
});
const mockedFont = {};
const mockCalcValues = {
font: mockedFont,
fontSize: 12,
characterSpacing: 1,
boxWidthInPt: 5,
};
it('does not split an empty string', () => {
expect(getSplittedLines('', mockCalcValues)).toEqual(['']);
});
it('does not split a short line', () => {
expect(getSplittedLines('a', mockCalcValues)).toEqual(['a']);
expect(getSplittedLines('aaaa', mockCalcValues)).toEqual(['aaaa']);
});
it('splits a line to the nearest previous breakable char', () => {
expect(getSplittedLines('aaa bbb', mockCalcValues)).toEqual(['aaa', 'bbb']);
expect(getSplittedLines('top-hat', mockCalcValues)).toEqual(['top-', 'hat']);
expect(getSplittedLines('top—hat', mockCalcValues)).toEqual(['top—', 'hat']); // em dash
expect(getSplittedLines('top–hat', mockCalcValues)).toEqual(['top–', 'hat']); // en dash
});
it('splits a line where the split point is on a breakable char', () => {
expect(getSplittedLines('aaaaa bbbbb', mockCalcValues)).toEqual(['aaaaa', 'bbbbb']);
expect(getSplittedLines('left-hand', mockCalcValues)).toEqual(['left-', 'hand']);
});
it('splits a long line in the middle of a word if too long', () => {
expect(getSplittedLines('aaaaaa bbb', mockCalcValues)).toEqual(['aaaaa', 'a bbb']);
expect(getSplittedLines('aaaaaa-a b', mockCalcValues)).toEqual(['aaaaa', 'a-a b']);
expect(getSplittedLines('aaaaa-aa b', mockCalcValues)).toEqual(['aaaaa', '-aa b']);
});
it('splits a long line without breakable chars at exactly 5 chars', () => {
expect(getSplittedLines('abcdef', mockCalcValues)).toEqual(['abcde', 'f']);
});
it('splits a very long line without breakable chars at exactly 5 chars', () => {
expect(getSplittedLines('abcdefghijklmn', mockCalcValues)).toEqual(['abcde', 'fghij', 'klmn']);
});
it('splits a line with lots of words', () => {
expect(getSplittedLines('a b c d e', mockCalcValues)).toEqual(['a b c', 'd e']);
});
});
describe('getSplittedLines test with real font width calculations', () => {
const font = getDefaultFont();
const baseCalcValues = {
fontSize: 12,
characterSpacing: 1,
boxWidthInPt: 40,
};
it('should not split a line when the text is shorter than the width', async () => {
const _cache = new Map();
await getFontKitFont(getTextSchema().fontName, font, _cache).then((fontKitFont) => {
const fontWidthCalcs = Object.assign({}, baseCalcValues, { font: fontKitFont });
const result = getSplittedLines('short', fontWidthCalcs);
expect(result).toEqual(['short']);
});
});
it('should split a line when the text is longer than the width', async () => {
const _cache = new Map();
await getFontKitFont(getTextSchema().fontName, font, _cache).then((fontKitFont) => {
const fontWidthCalcs = Object.assign({}, baseCalcValues, { font: fontKitFont });
const result = getSplittedLines('this will wrap', fontWidthCalcs);
expect(result).toEqual(['this', 'will', 'wrap']);
});
});
it('should split a line in the middle when unspaced text will not fit on a line', async () => {
const _cache = new Map();
await getFontKitFont(getTextSchema().fontName, font, _cache).then((fontKitFont) => {
const fontWidthCalcs = Object.assign({}, baseCalcValues, { font: fontKitFont });
const result = getSplittedLines('thiswillbecut', fontWidthCalcs);
expect(result).toEqual(['thiswi', 'llbecu', 't']);
});
});
it('should not split text when it is impossible due to size constraints', async () => {
const _cache = new Map();
await getFontKitFont(getTextSchema().fontName, font, _cache).then((fontKitFont) => {
const fontWidthCalcs = Object.assign({}, baseCalcValues, { font: fontKitFont });
fontWidthCalcs.boxWidthInPt = 2;
const result = getSplittedLines('thiswillnotbecut', fontWidthCalcs);
expect(result).toEqual(['thiswillnotbecut']);
});
});
});
describe('calculateDynamicFontSize with Default font', () => {
let fontKitFont;
beforeAll(async () => {
fontKitFont = await getFontKitFont('SauceHanSansJP', getDefaultFont(), new Map());
});
it('should return default font size when dynamicFontSizeSetting is not provided', async () => {
const textSchema = getTextSchema();
const result = calculateDynamicFontSize({ textSchema, fontKitFont, value: 'test' });
expect(result).toBe(14);
});
it('should return default font size when dynamicFontSizeSetting max is less than min', async () => {
const textSchema = getTextSchema();
textSchema.dynamicFontSize = { min: 11, max: 10, fit: 'vertical' };
const result = calculateDynamicFontSize({ textSchema, fontKitFont, value: 'test' });
expect(result).toBe(14);
});
it('should calculate a dynamic font size of vertical fit between min and max', async () => {
const textSchema = getTextSchema();
textSchema.dynamicFontSize = { min: 10, max: 30, fit: 'vertical' };
const value = 'test with a length string\n and a new line';
const result = calculateDynamicFontSize({ textSchema, fontKitFont, value });
expect(result).toBe(19.25);
});
it('should calculate a dynamic font size of horizontal fit between min and max', async () => {
const textSchema = getTextSchema();
textSchema.dynamicFontSize = { min: 10, max: 30, fit: 'horizontal' };
const value = 'test with a length string\n and a new line';
const result = calculateDynamicFontSize({ textSchema, fontKitFont, value });
expect(result).toBe(11.25);
});
it('should calculate a dynamic font size between min and max regardless of current font size', async () => {
const textSchema = getTextSchema();
textSchema.fontSize = 2;
textSchema.dynamicFontSize = { min: 10, max: 30, fit: 'vertical' };
const value = 'test with a length string\n and a new line';
let result = calculateDynamicFontSize({ textSchema, fontKitFont, value });
expect(result).toBe(19.25);
textSchema.fontSize = 40;
result = calculateDynamicFontSize({ textSchema, fontKitFont, value });
expect(result).toBe(19.25);
});
it('should return min font size when content is too big to fit given constraints', async () => {
const textSchema = getTextSchema();
textSchema.width = 10;
textSchema.dynamicFontSize = { min: 10, max: 30, fit: 'vertical' };
const value = 'test with a length string\n and a new line';
const result = calculateDynamicFontSize({ textSchema, fontKitFont, value });
expect(result).toBe(10);
});
it('should return max font size when content is too small to fit given constraints', async () => {
const textSchema = getTextSchema();
textSchema.width = 1000;
textSchema.height = 200;
textSchema.dynamicFontSize = { min: 10, max: 30, fit: 'vertical' };
const value = 'test with a length string\n and a new line';
const result = calculateDynamicFontSize({ textSchema, fontKitFont, value });
expect(result).toBe(30);
});
it('should not reduce font size below 0', async () => {
const textSchema = getTextSchema();
textSchema.dynamicFontSize = { min: -5, max: 10, fit: 'vertical' };
textSchema.width = 4;
textSchema.height = 1;
const value = 'a very \nlong \nmulti-line \nstring\nto';
const result = calculateDynamicFontSize({ textSchema, fontKitFont, value });
expect(result).toBeGreaterThan(0);
});
it('should calculate a dynamic font size when a starting font size is passed that is lower than the eventual', async () => {
const textSchema = getTextSchema();
textSchema.dynamicFontSize = { min: 10, max: 30, fit: 'vertical' };
const value = 'test with a length string\n and a new line';
const startingFontSize = 18;
const result = calculateDynamicFontSize({ textSchema, fontKitFont, value, startingFontSize });
expect(result).toBe(19.25);
});
it('should calculate a dynamic font size when a starting font size is passed that is higher than the eventual', async () => {
const textSchema = getTextSchema();
textSchema.dynamicFontSize = { min: 10, max: 30, fit: 'horizontal' };
const value = 'test with a length string\n and a new line';
const startingFontSize = 36;
const result = calculateDynamicFontSize({ textSchema, fontKitFont, value, startingFontSize });
expect(result).toBe(11.25);
});
it('should calculate a dynamic font size using vertical fit as a default if no fit provided', async () => {
const textSchema = getTextSchema();
textSchema.dynamicFontSize = { min: 10, max: 30, fit: 'vertical' };
const value = 'test with a length string\n and a new line';
const result = calculateDynamicFontSize({ textSchema, fontKitFont, value });
expect(result).toBe(19.25);
});
});
describe('calculateDynamicFontSize with Custom font', () => {
let fontKitFont;
beforeAll(async () => {
fontKitFont = await getFontKitFont('SauceHanSansJP', getSampleFont(), new Map());
});
it('should return smaller font size when dynamicFontSizeSetting is provided with horizontal fit', async () => {
const textSchema = getTextSchema();
textSchema.dynamicFontSize = { min: 10, max: 30, fit: 'horizontal' };
const value = 'あいうあいうあい';
const result = calculateDynamicFontSize({ textSchema, fontKitFont, value });
expect(result).toBe(16.75);
});
it('should return smaller font size when dynamicFontSizeSetting is provided with vertical fit', async () => {
const textSchema = getTextSchema();
textSchema.dynamicFontSize = { min: 10, max: 30, fit: 'vertical' };
const value = 'あいうあいうあい';
const result = calculateDynamicFontSize({ textSchema, fontKitFont, value });
expect(result).toBe(26);
});
it('should return min font size when content is too big to fit given constraints', async () => {
const textSchema = getTextSchema();
textSchema.dynamicFontSize = { min: 20, max: 30, fit: 'vertical' };
const value = 'あいうあいうあいうあいうあいうあいうあいうあいうあいう';
const result = await calculateDynamicFontSize({ textSchema, fontKitFont, value });
expect(result).toBe(20);
});
it('should return max font size when content is too small to fit given constraints', async () => {
const textSchema = getTextSchema();
textSchema.dynamicFontSize = { min: 10, max: 30, fit: 'vertical' };
const value = 'あ';
const result = await calculateDynamicFontSize({ textSchema, fontKitFont, value });
expect(result).toBe(30);
});
it('should return min font size when content is multi-line with too many lines for the container', async () => {
const textSchema = getTextSchema();
textSchema.dynamicFontSize = { min: 5, max: 20, fit: 'vertical' };
const value = 'あ\nいう\nあ\nいう\nあ\nいう\nあ\nいう\nあ\nいう\nあ\nいう';
const result = await calculateDynamicFontSize({ textSchema, fontKitFont, value });
expect(result).toBe(5);
});
});
describe('getFontDescentInPt test', () => {
test('it gets a descent size relative to the font size', () => {
expect(getFontDescentInPt({ descent: -400, unitsPerEm: 1000 }, 12)).toBe(-4.800000000000001);
expect(getFontDescentInPt({ descent: 54, unitsPerEm: 1000 }, 20)).toBe(1.08);
expect(getFontDescentInPt({ descent: -512, unitsPerEm: 2048 }, 54)).toBe(-13.5);
});
});
describe('getBrowserVerticalFontAdjustments test', () => {
// Font with a base line-height of 1.349
const font = { ascent: 1037, descent: -312, unitsPerEm: 1000 };
test('it gets a top adjustment when vertically aligning top', () => {
expect(getBrowserVerticalFontAdjustments(font, 12, 1.0, 'top')).toEqual({
topAdj: 2.791301999999999,
bottomAdj: 0,
});
expect(getBrowserVerticalFontAdjustments(font, 36, 2.0, 'top')).toEqual({
topAdj: 8.373906,
bottomAdj: 0,
});
});
test('it gets a bottom adjustment when vertically aligning middle or bottom', () => {
expect(getBrowserVerticalFontAdjustments(font, 12, 1.0, 'bottom')).toEqual({
topAdj: 0,
bottomAdj: 2.791302,
});
expect(getBrowserVerticalFontAdjustments(font, 12, 1.15, 'middle')).toEqual({
topAdj: 0,
bottomAdj: 1.5916020000000004,
});
});
test('it does not get a bottom adjustment if the line height exceeds that of the font', () => {
expect(getBrowserVerticalFontAdjustments(font, 12, 1.35, 'bottom')).toEqual({
topAdj: 0,
bottomAdj: 0,
});
});
test('it does not get a bottom adjustment if the font base line-height is 1.0 or less', () => {
const thisFont = { ascent: 900, descent: -50, unitsPerEm: 1000 };
expect(getBrowserVerticalFontAdjustments(thisFont, 20, 1.0, 'bottom')).toEqual({
topAdj: 0,
bottomAdj: 0,
});
});
});
describe('filterStartJP', () => {
test('空の配列を渡すと空の配列を返す', () => {
expect(filterStartJP([])).toEqual([]);
});
test('禁則文字を含まない行はそのまま返す', () => {
const input = ['これは', '普通の', '文章です。'];
expect(filterStartJP(input)).toEqual(input);
});
test('行頭の禁則文字を前の行の末尾に移動する', () => {
const input = ['これは', '。文章', 'です'];
const expected = ['これは。', '文章', 'です'];
expect(filterStartJP(input)).toEqual(expected);
});
test('複数の禁則文字を正しく処理する', () => {
const input = ['これは', '。とても', '、長い', '」文章', 'です'];
const expected = ['これは。', 'とても、', '長い」', '文章', 'です'];
expect(filterStartJP(input)).toEqual(expected);
});
test('空の行を保持する', () => {
const input = ['これは', '', '。文章', 'です'];
const expected = ['これは。', '', '文章', 'です'];
expect(filterStartJP(input)).toEqual(expected);
});
test('1文字の行(禁則文字のみ)はそのまま保持する', () => {
const input = ['これは', '。', '文章', 'です'];
// const expected = ['これは。', '文章', 'です'];
const expected = ['これは', '。', '文章', 'です'];
expect(filterStartJP(input)).toEqual(expected);
});
test('すべての禁則文字を正しく処理する', () => {
const input = LINE_START_FORBIDDEN_CHARS.map((char) => ['この', char + '文字']).flat();
const expected = LINE_START_FORBIDDEN_CHARS.map((char) => [
'この' + char,
'文字',
]).flat();
expect(filterStartJP(input)).toEqual(expected);
});
});
describe('filterEndJP', () => {
test('空の配列を渡すと空の配列を返す', () => {
expect(filterEndJP([])).toEqual([]);
});
test('禁則文字を含まない行はそのまま返す', () => {
const input = ['これは', '普通の', '文章です。'];
expect(filterEndJP(input)).toEqual(input);
});
test('行末の禁則文字を次の行の先頭に移動する', () => {
const input = ['これは「', '文章', 'です。'];
const expected = ['これは', '「文章', 'です。'];
expect(filterEndJP(input)).toEqual(expected);
});
test('複数の禁則文字を正しく処理する', () => {
const input = ['これは「', '長い『', '文章(', 'です。'];
const expected = ['これは', '「長い', '『文章', '(です。'];
expect(filterEndJP(input)).toEqual(expected);
});
// Cant understand purpose of this test...
// test('空の行を保持する', () => {
// const input = ['これは「', '', '文章', 'です。'];
// const expected = ['これは', '「', '', '文章', 'です。'];
// expect(filterEndJP(input)).toEqual(expected);
// });
test('1文字の行(禁則文字のみ)はそのまま保持する', () => {
const input = ['これは', '「', '文章', 'です。'];
const expected = ['これは', '「', '文章', 'です。'];
expect(filterEndJP(input)).toEqual(expected);
});
test('すべての禁則文字を正しく処理する', () => {
const input = LINE_END_FORBIDDEN_CHARS.map((char) => ['これは' + char, '文章']).flat();
const expected = LINE_END_FORBIDDEN_CHARS.map((char) => [
'これは',
char + '文章',
]).flat();
expect(filterEndJP(input)).toEqual(expected);
});
test('最後の行の禁則文字は移動しない', () => {
const input = ['これは「', '文章「', 'です「'];
const expected = ['これは', '「文章', '「です「'];
expect(filterEndJP(input)).toEqual(expected);
});
});
//# sourceMappingURL=text.test.js.map