@pdfme/schemas 5.3.3 → 5.3.4-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/cjs/__tests__/text.test.js +107 -37
  2. package/dist/cjs/__tests__/text.test.js.map +1 -1
  3. package/dist/cjs/src/multiVariableText/uiRender.js +9 -5
  4. package/dist/cjs/src/multiVariableText/uiRender.js.map +1 -1
  5. package/dist/cjs/src/shapes/rectAndEllipse.js +3 -2
  6. package/dist/cjs/src/shapes/rectAndEllipse.js.map +1 -1
  7. package/dist/cjs/src/tables/propPanel.js +1 -1
  8. package/dist/cjs/src/text/constants.js +75 -1
  9. package/dist/cjs/src/text/constants.js.map +1 -1
  10. package/dist/cjs/src/text/extraFormatter.js +2 -1
  11. package/dist/cjs/src/text/extraFormatter.js.map +1 -1
  12. package/dist/cjs/src/text/helper.js +183 -7
  13. package/dist/cjs/src/text/helper.js.map +1 -1
  14. package/dist/cjs/src/text/icons/index.js +2 -1
  15. package/dist/cjs/src/text/icons/index.js.map +1 -1
  16. package/dist/cjs/src/text/pdfRender.js +21 -7
  17. package/dist/cjs/src/text/pdfRender.js.map +1 -1
  18. package/dist/cjs/src/text/propPanel.js +3 -3
  19. package/dist/cjs/src/text/uiRender.js +7 -14
  20. package/dist/cjs/src/text/uiRender.js.map +1 -1
  21. package/dist/esm/__tests__/text.test.js +108 -38
  22. package/dist/esm/__tests__/text.test.js.map +1 -1
  23. package/dist/esm/src/multiVariableText/uiRender.js +6 -2
  24. package/dist/esm/src/multiVariableText/uiRender.js.map +1 -1
  25. package/dist/esm/src/shapes/rectAndEllipse.js +3 -2
  26. package/dist/esm/src/shapes/rectAndEllipse.js.map +1 -1
  27. package/dist/esm/src/tables/propPanel.js +1 -1
  28. package/dist/esm/src/text/constants.js +74 -0
  29. package/dist/esm/src/text/constants.js.map +1 -1
  30. package/dist/esm/src/text/extraFormatter.js +4 -3
  31. package/dist/esm/src/text/extraFormatter.js.map +1 -1
  32. package/dist/esm/src/text/helper.js +181 -7
  33. package/dist/esm/src/text/helper.js.map +1 -1
  34. package/dist/esm/src/text/icons/index.js +2 -1
  35. package/dist/esm/src/text/icons/index.js.map +1 -1
  36. package/dist/esm/src/text/pdfRender.js +21 -7
  37. package/dist/esm/src/text/pdfRender.js.map +1 -1
  38. package/dist/esm/src/text/propPanel.js +3 -3
  39. package/dist/esm/src/text/uiRender.js +7 -14
  40. package/dist/esm/src/text/uiRender.js.map +1 -1
  41. package/dist/types/src/tables/types.d.ts +1 -1
  42. package/dist/types/src/text/constants.d.ts +3 -0
  43. package/dist/types/src/text/helper.d.ts +5 -4
  44. package/dist/types/src/text/icons/index.d.ts +1 -0
  45. package/dist/types/src/text/types.d.ts +1 -1
  46. package/dist/types/src/text/uiRender.d.ts +2 -1
  47. package/package.json +1 -1
  48. package/src/multiVariableText/uiRender.ts +7 -2
  49. package/src/shapes/rectAndEllipse.ts +3 -2
  50. package/src/tables/propPanel.ts +1 -1
  51. package/src/tables/types.ts +1 -1
  52. package/src/text/constants.ts +81 -0
  53. package/src/text/extraFormatter.ts +4 -1
  54. package/src/text/helper.ts +184 -12
  55. package/src/text/icons/index.ts +3 -0
  56. package/src/text/pdfRender.ts +28 -14
  57. package/src/text/propPanel.ts +3 -3
  58. package/src/text/types.ts +1 -1
  59. package/src/text/uiRender.ts +7 -14
@@ -1,4 +1,4 @@
1
- import { UIRenderProps } from '@pdfme/common';
1
+ import { getDefaultFont, UIRenderProps } from '@pdfme/common';
2
2
  import { MultiVariableTextSchema } from './types';
3
3
  import {
4
4
  uiRender as parentUiRender,
@@ -6,6 +6,7 @@ import {
6
6
  makeElementPlainTextContentEditable
7
7
  } from '../text/uiRender';
8
8
  import { isEditable } from '../utils';
9
+ import { getFontKitFont } from '../text/helper';
9
10
  import { substituteVariables } from './helper';
10
11
 
11
12
  export const uiRender = async (arg: UIRenderProps<MultiVariableTextSchema>) => {
@@ -65,6 +66,8 @@ const formUiRender = async (arg: UIRenderProps<MultiVariableTextSchema>) => {
65
66
  onChange,
66
67
  stopEditing,
67
68
  theme,
69
+ _cache,
70
+ options,
68
71
  } = arg;
69
72
  const rawText = schema.text;
70
73
 
@@ -76,8 +79,10 @@ const formUiRender = async (arg: UIRenderProps<MultiVariableTextSchema>) => {
76
79
  const variables: Record<string, string> = JSON.parse(value) || {}
77
80
  const variableIndices = getVariableIndices(rawText);
78
81
  const substitutedText = substituteVariables(rawText, variables);
82
+ const font = options?.font || getDefaultFont();
83
+ const fontKitFont = await getFontKitFont(schema.fontName, font, _cache);
79
84
 
80
- const textBlock = await buildStyledTextContainer(arg, substitutedText);
85
+ const textBlock = buildStyledTextContainer(arg, fontKitFont, substitutedText);
81
86
 
82
87
  // Construct content-editable spans for each variable within the string
83
88
  let inVarString = false;
@@ -1,6 +1,7 @@
1
1
  import { Plugin, Schema, mm2pt } from '@pdfme/common';
2
2
  import { HEX_COLOR_PATTERN } from '../constants.js';
3
3
  import { hex2PrintingColor, convertForPdfLayoutProps, createSvgStr } from '../utils.js';
4
+ import { toRadians } from '@pdfme/pdf-lib';
4
5
  import { Circle, Square } from 'lucide';
5
6
 
6
7
  interface ShapeSchema extends Schema {
@@ -57,8 +58,8 @@ const shape: Plugin<ShapeSchema> = {
57
58
  });
58
59
  } else if (schema.type === 'rectangle') {
59
60
  page.drawRectangle({
60
- x: position.x + borderWidth / 2,
61
- y: position.y + borderWidth / 2,
61
+ x: position.x + borderWidth * ((1 - Math.sin(toRadians(rotate))) / 2) + Math.tan(toRadians(rotate)) * (Math.PI ** 2),
62
+ y: position.y + borderWidth * ((1 + Math.sin(toRadians(rotate))) / 2) + Math.tan(toRadians(rotate)) * (Math.PI ** 2),
62
63
  width: width - borderWidth,
63
64
  height: height - borderWidth,
64
65
  ...drawOptions,
@@ -43,7 +43,7 @@ export const propPanel: PropPanel<TableSchema> = {
43
43
  type: 'string',
44
44
  widget: 'color',
45
45
  props: {
46
- disabledAlpha: true
46
+ disabledAlpha: true,
47
47
  },
48
48
  rules: [{ pattern: HEX_COLOR_PATTERN, message: i18n('validation.hexColor') }],
49
49
  },
@@ -43,7 +43,7 @@ export interface Styles {
43
43
  textColor: string;
44
44
  lineHeight: number;
45
45
  characterSpacing: number;
46
- alignment: 'left' | 'center' | 'right';
46
+ alignment: 'left' | 'center' | 'right' | 'justify';
47
47
  verticalAlignment: 'top' | 'middle' | 'bottom';
48
48
  fontSize: number;
49
49
  cellPadding: Spacing;
@@ -5,6 +5,7 @@ export const DEFAULT_FONT_SIZE = 13;
5
5
  export const ALIGN_LEFT = 'left' as ALIGNMENT;
6
6
  export const ALIGN_CENTER = 'center' as ALIGNMENT;
7
7
  export const ALIGN_RIGHT = 'right' as ALIGNMENT;
8
+ export const ALIGN_JUSTIFY = 'justify' as ALIGNMENT;
8
9
  export const DEFAULT_ALIGNMENT = ALIGN_LEFT;
9
10
  export const VERTICAL_ALIGN_TOP = 'top' as VERTICAL_ALIGNMENT;
10
11
  export const VERTICAL_ALIGN_MIDDLE = 'middle' as VERTICAL_ALIGNMENT;
@@ -21,3 +22,83 @@ export const DEFAULT_DYNAMIC_MIN_FONT_SIZE = 4;
21
22
 
22
23
  export const DEFAULT_DYNAMIC_MAX_FONT_SIZE = 72;
23
24
  export const FONT_SIZE_ADJUSTMENT = 0.25;
25
+
26
+ export const LINE_START_FORBIDDEN_CHARS = [
27
+ // 句読点
28
+ '、',
29
+ '。',
30
+ ',',
31
+ '.',
32
+
33
+ // 閉じカッコ類
34
+ '」',
35
+ '』',
36
+ ')',
37
+ '}',
38
+ '】',
39
+ '>',
40
+ '≫',
41
+ ']',
42
+
43
+ // 記号
44
+ '・',
45
+ 'ー',
46
+ '―',
47
+ '-',
48
+
49
+ // 約物
50
+ '!',
51
+ '!',
52
+ '?',
53
+ '?',
54
+ ':',
55
+ ':',
56
+ ';',
57
+ ';',
58
+ '/',
59
+ '/',
60
+
61
+ // 繰り返し記号
62
+ 'ゝ',
63
+ '々',
64
+ '〃',
65
+
66
+ // 拗音・促音(小書きのかな)
67
+ 'ぁ',
68
+ 'ぃ',
69
+ 'ぅ',
70
+ 'ぇ',
71
+ 'ぉ',
72
+ 'っ',
73
+ 'ゃ',
74
+ 'ゅ',
75
+ 'ょ',
76
+ 'ァ',
77
+ 'ィ',
78
+ 'ゥ',
79
+ 'ェ',
80
+ 'ォ',
81
+ 'ッ',
82
+ 'ャ',
83
+ 'ュ',
84
+ 'ョ',
85
+ ];
86
+
87
+ export const LINE_END_FORBIDDEN_CHARS = [
88
+ // 始め括弧類
89
+ '「',
90
+ '『',
91
+ '(',
92
+ '{',
93
+ '【',
94
+ '<',
95
+ '≪',
96
+ '[',
97
+ '〘',
98
+ '〖',
99
+ '〝',
100
+ '‘',
101
+ '“',
102
+ '⦅',
103
+ '«',
104
+ ];
@@ -3,6 +3,7 @@ import {
3
3
  TextAlignCenterIcon,
4
4
  TextAlignLeftIcon,
5
5
  TextAlignRightIcon,
6
+ TextAlignJustifyIcon,
6
7
  TextStrikethroughIcon,
7
8
  TextUnderlineIcon,
8
9
  TextVerticalAlignBottomIcon,
@@ -16,6 +17,7 @@ import {
16
17
  DEFAULT_VERTICAL_ALIGNMENT,
17
18
  VERTICAL_ALIGN_BOTTOM,
18
19
  VERTICAL_ALIGN_MIDDLE,
20
+ ALIGN_JUSTIFY,
19
21
  } from './constants';
20
22
 
21
23
  export enum Formatter {
@@ -52,6 +54,7 @@ export function getExtraFormatterSchema(i18n: (key: keyof Dict | string) => stri
52
54
  { key: Formatter.ALIGNMENT, icon: TextAlignLeftIcon, type: 'select', value: DEFAULT_ALIGNMENT },
53
55
  { key: Formatter.ALIGNMENT, icon: TextAlignCenterIcon, type: 'select', value: ALIGN_CENTER },
54
56
  { key: Formatter.ALIGNMENT, icon: TextAlignRightIcon, type: 'select', value: ALIGN_RIGHT },
57
+ { key: Formatter.ALIGNMENT, icon: TextAlignJustifyIcon, type: 'select', value: ALIGN_JUSTIFY },
55
58
  {
56
59
  key: Formatter.VERTICAL_ALIGNMENT,
57
60
  icon: TextVerticalAlignTopIcon,
@@ -75,6 +78,6 @@ export function getExtraFormatterSchema(i18n: (key: keyof Dict | string) => stri
75
78
  title: i18n('schemas.text.format'),
76
79
  widget: 'ButtonGroup',
77
80
  buttons,
78
- span: 17,
81
+ span: 24,
79
82
  };
80
83
  }
@@ -21,6 +21,8 @@ import {
21
21
  DYNAMIC_FIT_HORIZONTAL,
22
22
  DYNAMIC_FIT_VERTICAL,
23
23
  VERTICAL_ALIGN_TOP,
24
+ LINE_END_FORBIDDEN_CHARS,
25
+ LINE_START_FORBIDDEN_CHARS,
24
26
  } from './constants.js';
25
27
 
26
28
  export const getBrowserVerticalFontAdjustments = (
@@ -163,9 +165,9 @@ const getOverPosition = (textLine: string, calcValues: FontWidthCalcValues) => {
163
165
  * However, this might need to be revisited for broader language support.
164
166
  */
165
167
  const isLineBreakableChar = (char: string) => {
166
- const lineBreakableChars = [' ', '-', "\u2014", "\u2013"];
168
+ const lineBreakableChars = [' ', '-', '\u2014', '\u2013'];
167
169
  return lineBreakableChars.includes(char);
168
- }
170
+ };
169
171
 
170
172
  /**
171
173
  * Gets the position of the split. Splits the exceeding line at
@@ -183,7 +185,7 @@ const getSplitPosition = (textLine: string, calcValues: FontWidthCalcValues) =>
183
185
  let overPosTmp = overPos - 1;
184
186
  while (overPosTmp >= 0) {
185
187
  if (isLineBreakableChar(textLine[overPosTmp])) {
186
- return overPosTmp+1;
188
+ return overPosTmp + 1;
187
189
  }
188
190
  overPosTmp--;
189
191
  }
@@ -221,18 +223,16 @@ export const getSplittedLines = (textLine: string, calcValues: FontWidthCalcValu
221
223
  * Calculating space usage involves splitting lines where they exceed
222
224
  * the box width based on the proposed size.
223
225
  */
224
- export const calculateDynamicFontSize = async ({
226
+ export const calculateDynamicFontSize = ({
225
227
  textSchema,
226
- font,
228
+ fontKitFont,
227
229
  value,
228
230
  startingFontSize,
229
- _cache,
230
231
  }: {
231
232
  textSchema: TextSchema;
232
- font: Font;
233
+ fontKitFont: FontKitFont;
233
234
  value: string;
234
235
  startingFontSize?: number | undefined;
235
- _cache: Map<any, any>;
236
236
  }) => {
237
237
  const {
238
238
  fontSize: schemaFontSize,
@@ -247,7 +247,6 @@ export const calculateDynamicFontSize = async ({
247
247
  if (dynamicFontSizeSetting.max < dynamicFontSizeSetting.min) return fontSize;
248
248
 
249
249
  const characterSpacing = schemaCharacterSpacing ?? DEFAULT_CHARACTER_SPACING;
250
- const fontKitFont = await getFontKitFont(textSchema.fontName, font, _cache);
251
250
  const paragraphs = value.split('\n');
252
251
 
253
252
  let dynamicFontSize = fontSize;
@@ -268,16 +267,22 @@ export const calculateDynamicFontSize = async ({
268
267
  const otherRowHeightInMm = pt2mm(size * lineHeight);
269
268
 
270
269
  paragraphs.forEach((paragraph, paraIndex) => {
271
- const lines = getSplittedLines(paragraph, {
270
+ const lines = getSplittedLinesBySegmenter(paragraph, {
272
271
  font: fontKitFont,
273
272
  fontSize: size,
274
273
  characterSpacing,
275
274
  boxWidthInPt,
276
275
  });
276
+
277
277
  lines.forEach((line, lineIndex) => {
278
278
  if (dynamicFontFit === DYNAMIC_FIT_VERTICAL) {
279
279
  // For vertical fit we want to consider the width of text lines where we detect a split
280
- const textWidth = widthOfTextAtSize(line, fontKitFont, size, characterSpacing);
280
+ const textWidth = widthOfTextAtSize(
281
+ line.replace('\n', ''),
282
+ fontKitFont,
283
+ size,
284
+ characterSpacing
285
+ );
281
286
  const textWidthInMm = pt2mm(textWidth);
282
287
  totalWidthInMm = Math.max(totalWidthInMm, textWidthInMm);
283
288
  }
@@ -358,8 +363,175 @@ export const splitTextToSize = (arg: {
358
363
  };
359
364
  let lines: string[] = [];
360
365
  value.split(/\r\n|\r|\n|\f|\u000B/g).forEach((line: string) => {
361
- lines = lines.concat(getSplittedLines(line, fontWidthCalcValues));
366
+ lines = lines.concat(getSplittedLinesBySegmenter(line, fontWidthCalcValues));
362
367
  });
363
368
  return lines;
364
369
  };
365
370
  export const isFirefox = () => navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
371
+
372
+ const getSplittedLinesBySegmenter = (line: string, calcValues: FontWidthCalcValues): string[] => {
373
+ // nothing to process but need to keep this for new lines.
374
+ if (line.trim() === '') {
375
+ return [''];
376
+ }
377
+
378
+ const { font, fontSize, characterSpacing, boxWidthInPt } = calcValues;
379
+ const segmenter = new Intl.Segmenter(undefined, { granularity: 'word' });
380
+ const iterator = segmenter.segment(line.trimEnd())[Symbol.iterator]();
381
+
382
+ let lines: string[] = [];
383
+ let lineCounter: number = 0;
384
+ let currentTextSize: number = 0;
385
+
386
+ while (true) {
387
+ const chunk = iterator.next();
388
+ if (chunk.done) break;
389
+ const segment = chunk.value.segment;
390
+ const textWidth = widthOfTextAtSize(segment, font, fontSize, characterSpacing);
391
+ if (currentTextSize + textWidth <= boxWidthInPt) {
392
+ // the size of boxWidth is large enough to add the segment
393
+ if (lines[lineCounter]) {
394
+ lines[lineCounter] += segment;
395
+ currentTextSize += textWidth + characterSpacing;
396
+ } else {
397
+ lines[lineCounter] = segment;
398
+ currentTextSize = textWidth + characterSpacing;
399
+ }
400
+ } else if (segment.trim() === '') {
401
+ // a segment can be consist of multiple spaces like ' '
402
+ // if they overflow the box, treat them as a line break and move to the next line
403
+ lines[++lineCounter] = '';
404
+ currentTextSize = 0;
405
+ } else if (textWidth <= boxWidthInPt) {
406
+ // the segment is small enough to be added to the next line
407
+ lines[++lineCounter] = segment;
408
+ currentTextSize = textWidth + characterSpacing;
409
+ } else {
410
+ // the segment is too large to fit in the boxWidth, we wrap the segment
411
+ for (const char of segment) {
412
+ const size = widthOfTextAtSize(char, font, fontSize, characterSpacing);
413
+ if (currentTextSize + size <= boxWidthInPt) {
414
+ if (lines[lineCounter]) {
415
+ lines[lineCounter] += char;
416
+ currentTextSize += size + characterSpacing;
417
+ } else {
418
+ lines[lineCounter] = char;
419
+ currentTextSize = size + characterSpacing;
420
+ }
421
+ } else {
422
+ lines[++lineCounter] = char;
423
+ currentTextSize = size + characterSpacing;
424
+ }
425
+ }
426
+ }
427
+ }
428
+
429
+ if (lines.some(containsJapanese)) {
430
+ return adjustEndOfLine(filterEndJP(filterStartJP(lines)));
431
+ } else {
432
+ return adjustEndOfLine(lines);
433
+ }
434
+ };
435
+
436
+ // add a newline if the line is the end of the paragraph
437
+ const adjustEndOfLine = (lines: string[]): string[] => {
438
+ return lines.map((line, index) => {
439
+ if (index === lines.length - 1) {
440
+ return line.trimEnd() + '\n';
441
+ } else {
442
+ return line.trimEnd();
443
+ }
444
+ });
445
+ };
446
+
447
+ function containsJapanese(text: string): boolean {
448
+ return /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u.test(text);
449
+ }
450
+ //
451
+ // 日本語禁則処理
452
+ //
453
+ // https://www.morisawa.co.jp/blogs/MVP/8760
454
+ //
455
+ // 行頭禁則
456
+ export const filterStartJP = (lines: string[]): string[] => {
457
+ const filtered: string[] = [];
458
+ let charToAppend: string | null = null;
459
+
460
+ lines
461
+ .slice()
462
+ .reverse()
463
+ .forEach((line) => {
464
+ if (line.trim().length === 0) {
465
+ filtered.push('');
466
+ } else {
467
+ const charAtStart: string = line.charAt(0);
468
+ if (LINE_START_FORBIDDEN_CHARS.includes(charAtStart)) {
469
+ if (line.trim().length === 1) {
470
+ filtered.push(line);
471
+ charToAppend = null;
472
+ } else {
473
+ if (charToAppend) {
474
+ filtered.push(line.slice(1) + charToAppend);
475
+ } else {
476
+ filtered.push(line.slice(1));
477
+ }
478
+ charToAppend = charAtStart;
479
+ }
480
+ } else {
481
+ if (charToAppend) {
482
+ filtered.push(line + charToAppend);
483
+ charToAppend = null;
484
+ } else {
485
+ filtered.push(line);
486
+ }
487
+ }
488
+ }
489
+ });
490
+
491
+ if (charToAppend) {
492
+ return [charToAppend + filtered.slice(0, 1)[0], ...filtered.slice(1)].reverse();
493
+ } else {
494
+ return filtered.reverse();
495
+ }
496
+ };
497
+
498
+ // 行末禁則
499
+ export const filterEndJP = (lines: string[]): string[] => {
500
+ const filtered: string[] = [];
501
+ let charToPrepend: string | null = null;
502
+
503
+ lines.forEach((line) => {
504
+ if (line.trim().length === 0) {
505
+ filtered.push('');
506
+ } else {
507
+ const chartAtEnd = line.slice(-1);
508
+
509
+ if (LINE_END_FORBIDDEN_CHARS.includes(chartAtEnd)) {
510
+ if (line.trim().length === 1) {
511
+ filtered.push(line);
512
+ charToPrepend = null;
513
+ } else {
514
+ if (charToPrepend) {
515
+ filtered.push(charToPrepend + line.slice(0, -1));
516
+ } else {
517
+ filtered.push(line.slice(0, -1));
518
+ }
519
+ charToPrepend = chartAtEnd;
520
+ }
521
+ } else {
522
+ if (charToPrepend) {
523
+ filtered.push(charToPrepend + line);
524
+ charToPrepend = null;
525
+ } else {
526
+ filtered.push(line);
527
+ }
528
+ }
529
+ }
530
+ });
531
+
532
+ if (charToPrepend) {
533
+ return [...filtered.slice(0, -1), filtered.slice(-1)[0] + charToPrepend];
534
+ } else {
535
+ return filtered;
536
+ }
537
+ };
@@ -6,6 +6,7 @@ import {
6
6
  AlignRight,
7
7
  ArrowUpToLine,
8
8
  ArrowDownToLine,
9
+ AlignJustify,
9
10
  } from 'lucide';
10
11
  import { createSvgStr } from '../../utils.js';
11
12
 
@@ -19,6 +20,8 @@ export const TextAlignCenterIcon = createSvgStr(AlignCenter);
19
20
 
20
21
  export const TextAlignRightIcon = createSvgStr(AlignRight);
21
22
 
23
+ export const TextAlignJustifyIcon = createSvgStr(AlignJustify);
24
+
22
25
  export const TextVerticalAlignTopIcon = createSvgStr(ArrowUpToLine);
23
26
 
24
27
  // svg icons are material icons from https://www.xicons.org
@@ -1,4 +1,5 @@
1
1
  import { PDFFont, PDFDocument } from '@pdfme/pdf-lib';
2
+ import type { Font as FontKitFont } from 'fontkit';
2
3
  import type { TextSchema } from './types';
3
4
  import {
4
5
  PDFRenderProps,
@@ -60,21 +61,19 @@ const embedAndGetFontObj = async (arg: {
60
61
  return fontObj;
61
62
  };
62
63
 
63
- const getFontProp = async ({
64
+ const getFontProp = ({
64
65
  value,
65
- font,
66
+ fontKitFont,
66
67
  schema,
67
68
  colorType,
68
- _cache,
69
69
  }: {
70
70
  value: string;
71
- font: Font;
71
+ fontKitFont: FontKitFont;
72
72
  colorType?: ColorType;
73
73
  schema: TextSchema;
74
- _cache: Map<any, any>;
75
74
  }) => {
76
75
  const fontSize = schema.dynamicFontSize
77
- ? await calculateDynamicFontSize({ textSchema: schema, font, value, _cache })
76
+ ? calculateDynamicFontSize({ textSchema: schema, fontKitFont, value })
78
77
  : schema.fontSize ?? DEFAULT_FONT_SIZE;
79
78
  const color = hex2PrintingColor(schema.fontColor || DEFAULT_FONT_COLOR, colorType);
80
79
 
@@ -94,11 +93,11 @@ export const pdfRender = async (arg: PDFRenderProps<TextSchema>) => {
94
93
 
95
94
  const { font = getDefaultFont(), colorType } = options;
96
95
 
97
- const [pdfFontObj, fontKitFont, fontProp] = await Promise.all([
96
+ const [pdfFontObj, fontKitFont] = await Promise.all([
98
97
  embedAndGetFontObj({ pdfDoc, font, _cache }),
99
98
  getFontKitFont(schema.fontName, font, _cache),
100
- getFontProp({ value, font, schema, _cache, colorType }),
101
99
  ]);
100
+ const fontProp = getFontProp({ value, fontKitFont, schema, colorType });
102
101
 
103
102
  const { fontSize, color, alignment, verticalAlignment, lineHeight, characterSpacing } = fontProp;
104
103
 
@@ -121,8 +120,6 @@ export const pdfRender = async (arg: PDFRenderProps<TextSchema>) => {
121
120
  page.drawRectangle({ x, y, width, height, rotate, color });
122
121
  }
123
122
 
124
- page.pushOperators(pdfLib.setCharacterSpacing(characterSpacing ?? DEFAULT_CHARACTER_SPACING));
125
-
126
123
  const firstLineTextHeight = heightOfFontAtSize(fontKitFont, fontSize);
127
124
  const descent = getFontDescentInPt(fontKitFont, fontSize);
128
125
  const halfLineHeightAdjustment = lineHeight === 0 ? 0 : ((lineHeight - 1) * fontSize) / 2;
@@ -151,12 +148,20 @@ export const pdfRender = async (arg: PDFRenderProps<TextSchema>) => {
151
148
  }
152
149
 
153
150
  const pivotPoint = { x: x + width / 2, y: pageHeight - mm2pt(schema.position.y) - height / 2 };
151
+ const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
154
152
 
155
153
  lines.forEach((line, rowIndex) => {
156
- const textWidth = widthOfTextAtSize(line, fontKitFont, fontSize, characterSpacing);
154
+ const trimmed = line.replace('\n', '');
155
+ const textWidth = widthOfTextAtSize(trimmed, fontKitFont, fontSize, characterSpacing);
157
156
  const textHeight = heightOfFontAtSize(fontKitFont, fontSize);
158
157
  const rowYOffset = lineHeight * fontSize * rowIndex;
159
158
 
159
+ // Adobe Acrobat Reader shows an error if `drawText` is called with an empty text
160
+ if (line === '') {
161
+ // return; // this also works
162
+ line = '\r\n';
163
+ }
164
+
160
165
  let xLine = x;
161
166
  if (alignment === 'center') {
162
167
  xLine += (width - textWidth) / 2;
@@ -168,7 +173,7 @@ export const pdfRender = async (arg: PDFRenderProps<TextSchema>) => {
168
173
 
169
174
  // draw strikethrough
170
175
  if (schema.strikethrough && textWidth > 0) {
171
- const _x = xLine + textWidth + 1
176
+ const _x = xLine + textWidth + 1;
172
177
  const _y = yLine + textHeight / 3;
173
178
  page.drawLine({
174
179
  start: rotatePoint({ x: xLine, y: _y }, pivotPoint, rotate.angle),
@@ -181,7 +186,7 @@ export const pdfRender = async (arg: PDFRenderProps<TextSchema>) => {
181
186
 
182
187
  // draw underline
183
188
  if (schema.underline && textWidth > 0) {
184
- const _x = xLine + textWidth + 1
189
+ const _x = xLine + textWidth + 1;
185
190
  const _y = yLine - textHeight / 12;
186
191
  page.drawLine({
187
192
  start: rotatePoint({ x: xLine, y: _y }, pivotPoint, rotate.angle),
@@ -200,7 +205,16 @@ export const pdfRender = async (arg: PDFRenderProps<TextSchema>) => {
200
205
  yLine = rotatedPoint.y;
201
206
  }
202
207
 
203
- page.drawText(line, {
208
+ let spacing = characterSpacing;
209
+ if (alignment === 'justify' && line.slice(-1) !== '\n') {
210
+ // if alignment is `justify` but the end of line is not newline, then adjust the spacing
211
+ const iterator = segmenter.segment(trimmed)[Symbol.iterator]();
212
+ const len = Array.from(iterator).length;
213
+ spacing += (width - textWidth) / len;
214
+ }
215
+ page.pushOperators(pdfLib.setCharacterSpacing(spacing));
216
+
217
+ page.drawText(trimmed, {
204
218
  x: xLine,
205
219
  y: yLine,
206
220
  rotate,
@@ -87,7 +87,7 @@ export const propPanel: PropPanel<TextSchema> = {
87
87
  type: 'number',
88
88
  widget: 'inputNumber',
89
89
  props: { step: 0.1, min: 0 },
90
- span: 7,
90
+ span: 8,
91
91
  },
92
92
  useDynamicFontSize: { type: 'boolean', widget: 'UseDynamicFontSize', bind: false, span: 16 },
93
93
  dynamicFontSize: {
@@ -128,7 +128,7 @@ export const propPanel: PropPanel<TextSchema> = {
128
128
  type: 'string',
129
129
  widget: 'color',
130
130
  props: {
131
- disabledAlpha: true
131
+ disabledAlpha: true,
132
132
  },
133
133
  rules: [
134
134
  {
@@ -142,7 +142,7 @@ export const propPanel: PropPanel<TextSchema> = {
142
142
  type: 'string',
143
143
  widget: 'color',
144
144
  props: {
145
- disabledAlpha: true
145
+ disabledAlpha: true,
146
146
  },
147
147
  rules: [
148
148
  {
package/src/text/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Schema } from '@pdfme/common';
2
2
  import type { Font as FontKitFont } from 'fontkit';
3
3
 
4
- export type ALIGNMENT = 'left' | 'center' | 'right';
4
+ export type ALIGNMENT = 'left' | 'center' | 'right' | 'justify';
5
5
  export type VERTICAL_ALIGNMENT = 'top' | 'middle' | 'bottom';
6
6
  export type DYNAMIC_FONT_SIZE_FIT = 'horizontal' | 'vertical';
7
7
 
@@ -69,10 +69,8 @@ export const uiRender = async (arg: UIRenderProps<TextSchema>) => {
69
69
  return text;
70
70
  };
71
71
  const font = options?.font || getDefaultFont();
72
- const [fontKitFont, textBlock] = await Promise.all([
73
- getFontKitFont(schema.fontName, font, _cache),
74
- buildStyledTextContainer(arg, usePlaceholder ? placeholder : value),
75
- ]);
72
+ const fontKitFont = await getFontKitFont(schema.fontName, font, _cache);
73
+ const textBlock = buildStyledTextContainer(arg, fontKitFont, usePlaceholder ? placeholder : value);
76
74
 
77
75
  const processedText = replaceUnsupportedChars(value, fontKitFont);
78
76
 
@@ -100,19 +98,16 @@ export const uiRender = async (arg: UIRenderProps<TextSchema>) => {
100
98
 
101
99
  if (schema.dynamicFontSize) {
102
100
  let dynamicFontSize: undefined | number = undefined;
103
- const font = options?.font || getDefaultFont();
104
- const fontKitFont = await getFontKitFont(schema.fontName, font, _cache);
105
101
 
106
102
  textBlock.addEventListener('keyup', () => {
107
103
  setTimeout(() => {
108
104
  void (async () => {
109
105
  if (!textBlock.textContent) return;
110
- dynamicFontSize = await calculateDynamicFontSize({
106
+ dynamicFontSize = calculateDynamicFontSize({
111
107
  textSchema: schema,
112
- font,
108
+ fontKitFont,
113
109
  value: getText(textBlock),
114
110
  startingFontSize: dynamicFontSize,
115
- _cache,
116
111
  });
117
112
  textBlock.style.fontSize = `${dynamicFontSize}pt`;
118
113
 
@@ -155,23 +150,21 @@ export const uiRender = async (arg: UIRenderProps<TextSchema>) => {
155
150
  }
156
151
  };
157
152
 
158
- export const buildStyledTextContainer = async (arg: UIRenderProps<TextSchema>, value: string) => {
153
+ export const buildStyledTextContainer = (arg: UIRenderProps<TextSchema>, fontKitFont: FontKitFont, value: string) => {
159
154
  const { schema, rootElement, mode, options, _cache } = arg;
160
155
  const font = options?.font || getDefaultFont();
161
156
 
162
157
  let dynamicFontSize: undefined | number = undefined;
163
158
 
164
159
  if (schema.dynamicFontSize && value) {
165
- dynamicFontSize = await calculateDynamicFontSize({
160
+ dynamicFontSize = calculateDynamicFontSize({
166
161
  textSchema: schema,
167
- font,
162
+ fontKitFont,
168
163
  value,
169
164
  startingFontSize: dynamicFontSize,
170
- _cache,
171
165
  });
172
166
  }
173
167
 
174
- const fontKitFont = await getFontKitFont(schema.fontName, font, _cache);
175
168
  // Depending on vertical alignment, we need to move the top or bottom of the font to keep
176
169
  // it within it's defined box and align it with the generated pdf.
177
170
  const { topAdj, bottomAdj } = getBrowserVerticalFontAdjustments(