@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.
- package/dist/cjs/__tests__/text.test.js +107 -37
- package/dist/cjs/__tests__/text.test.js.map +1 -1
- package/dist/cjs/src/multiVariableText/uiRender.js +9 -5
- package/dist/cjs/src/multiVariableText/uiRender.js.map +1 -1
- package/dist/cjs/src/shapes/rectAndEllipse.js +3 -2
- package/dist/cjs/src/shapes/rectAndEllipse.js.map +1 -1
- package/dist/cjs/src/tables/propPanel.js +1 -1
- package/dist/cjs/src/text/constants.js +75 -1
- package/dist/cjs/src/text/constants.js.map +1 -1
- package/dist/cjs/src/text/extraFormatter.js +2 -1
- package/dist/cjs/src/text/extraFormatter.js.map +1 -1
- package/dist/cjs/src/text/helper.js +183 -7
- package/dist/cjs/src/text/helper.js.map +1 -1
- package/dist/cjs/src/text/icons/index.js +2 -1
- package/dist/cjs/src/text/icons/index.js.map +1 -1
- package/dist/cjs/src/text/pdfRender.js +21 -7
- package/dist/cjs/src/text/pdfRender.js.map +1 -1
- package/dist/cjs/src/text/propPanel.js +3 -3
- package/dist/cjs/src/text/uiRender.js +7 -14
- package/dist/cjs/src/text/uiRender.js.map +1 -1
- package/dist/esm/__tests__/text.test.js +108 -38
- package/dist/esm/__tests__/text.test.js.map +1 -1
- package/dist/esm/src/multiVariableText/uiRender.js +6 -2
- package/dist/esm/src/multiVariableText/uiRender.js.map +1 -1
- package/dist/esm/src/shapes/rectAndEllipse.js +3 -2
- package/dist/esm/src/shapes/rectAndEllipse.js.map +1 -1
- package/dist/esm/src/tables/propPanel.js +1 -1
- package/dist/esm/src/text/constants.js +74 -0
- package/dist/esm/src/text/constants.js.map +1 -1
- package/dist/esm/src/text/extraFormatter.js +4 -3
- package/dist/esm/src/text/extraFormatter.js.map +1 -1
- package/dist/esm/src/text/helper.js +181 -7
- package/dist/esm/src/text/helper.js.map +1 -1
- package/dist/esm/src/text/icons/index.js +2 -1
- package/dist/esm/src/text/icons/index.js.map +1 -1
- package/dist/esm/src/text/pdfRender.js +21 -7
- package/dist/esm/src/text/pdfRender.js.map +1 -1
- package/dist/esm/src/text/propPanel.js +3 -3
- package/dist/esm/src/text/uiRender.js +7 -14
- package/dist/esm/src/text/uiRender.js.map +1 -1
- package/dist/types/src/tables/types.d.ts +1 -1
- package/dist/types/src/text/constants.d.ts +3 -0
- package/dist/types/src/text/helper.d.ts +5 -4
- package/dist/types/src/text/icons/index.d.ts +1 -0
- package/dist/types/src/text/types.d.ts +1 -1
- package/dist/types/src/text/uiRender.d.ts +2 -1
- package/package.json +1 -1
- package/src/multiVariableText/uiRender.ts +7 -2
- package/src/shapes/rectAndEllipse.ts +3 -2
- package/src/tables/propPanel.ts +1 -1
- package/src/tables/types.ts +1 -1
- package/src/text/constants.ts +81 -0
- package/src/text/extraFormatter.ts +4 -1
- package/src/text/helper.ts +184 -12
- package/src/text/icons/index.ts +3 -0
- package/src/text/pdfRender.ts +28 -14
- package/src/text/propPanel.ts +3 -3
- package/src/text/types.ts +1 -1
- 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 =
|
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,
|
package/src/tables/propPanel.ts
CHANGED
package/src/tables/types.ts
CHANGED
@@ -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;
|
package/src/text/constants.ts
CHANGED
@@ -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:
|
81
|
+
span: 24,
|
79
82
|
};
|
80
83
|
}
|
package/src/text/helper.ts
CHANGED
@@ -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 = [' ', '-',
|
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 =
|
226
|
+
export const calculateDynamicFontSize = ({
|
225
227
|
textSchema,
|
226
|
-
|
228
|
+
fontKitFont,
|
227
229
|
value,
|
228
230
|
startingFontSize,
|
229
|
-
_cache,
|
230
231
|
}: {
|
231
232
|
textSchema: TextSchema;
|
232
|
-
|
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 =
|
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(
|
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(
|
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
|
+
};
|
package/src/text/icons/index.ts
CHANGED
@@ -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
|
package/src/text/pdfRender.ts
CHANGED
@@ -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 =
|
64
|
+
const getFontProp = ({
|
64
65
|
value,
|
65
|
-
|
66
|
+
fontKitFont,
|
66
67
|
schema,
|
67
68
|
colorType,
|
68
|
-
_cache,
|
69
69
|
}: {
|
70
70
|
value: string;
|
71
|
-
|
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
|
-
?
|
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
|
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
|
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
|
-
|
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,
|
package/src/text/propPanel.ts
CHANGED
@@ -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:
|
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
|
|
package/src/text/uiRender.ts
CHANGED
@@ -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
|
73
|
-
|
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 =
|
106
|
+
dynamicFontSize = calculateDynamicFontSize({
|
111
107
|
textSchema: schema,
|
112
|
-
|
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 =
|
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 =
|
160
|
+
dynamicFontSize = calculateDynamicFontSize({
|
166
161
|
textSchema: schema,
|
167
|
-
|
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(
|