@pdfme/common 2.0.2 → 2.1.0

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 (40) hide show
  1. package/dist/cjs/__tests__/font.test.js +193 -27
  2. package/dist/cjs/__tests__/font.test.js.map +1 -1
  3. package/dist/cjs/__tests__/helper.test.js +15 -0
  4. package/dist/cjs/__tests__/helper.test.js.map +1 -1
  5. package/dist/cjs/src/constants.js +8 -5
  6. package/dist/cjs/src/constants.js.map +1 -1
  7. package/dist/cjs/src/font.js +147 -38
  8. package/dist/cjs/src/font.js.map +1 -1
  9. package/dist/cjs/src/helper.js +10 -1
  10. package/dist/cjs/src/helper.js.map +1 -1
  11. package/dist/cjs/src/index.js +11 -5
  12. package/dist/cjs/src/index.js.map +1 -1
  13. package/dist/cjs/src/schema.js +1 -0
  14. package/dist/cjs/src/schema.js.map +1 -1
  15. package/dist/esm/__tests__/font.test.js +194 -28
  16. package/dist/esm/__tests__/font.test.js.map +1 -1
  17. package/dist/esm/__tests__/helper.test.js +16 -1
  18. package/dist/esm/__tests__/helper.test.js.map +1 -1
  19. package/dist/esm/src/constants.js +7 -4
  20. package/dist/esm/src/constants.js.map +1 -1
  21. package/dist/esm/src/font.js +145 -38
  22. package/dist/esm/src/font.js.map +1 -1
  23. package/dist/esm/src/helper.js +8 -1
  24. package/dist/esm/src/helper.js.map +1 -1
  25. package/dist/esm/src/index.js +4 -4
  26. package/dist/esm/src/index.js.map +1 -1
  27. package/dist/esm/src/schema.js +1 -0
  28. package/dist/esm/src/schema.js.map +1 -1
  29. package/dist/types/src/constants.d.ts +7 -4
  30. package/dist/types/src/font.d.ts +20 -1
  31. package/dist/types/src/helper.d.ts +2 -0
  32. package/dist/types/src/index.d.ts +5 -5
  33. package/dist/types/src/schema.d.ts +94 -0
  34. package/dist/types/src/type.d.ts +1 -0
  35. package/package.json +1 -1
  36. package/src/constants.ts +7 -4
  37. package/src/font.ts +195 -63
  38. package/src/helper.ts +10 -1
  39. package/src/index.ts +24 -9
  40. package/src/schema.ts +1 -0
package/src/font.ts CHANGED
@@ -7,11 +7,14 @@ import {
7
7
  DEFAULT_FONT_NAME,
8
8
  DEFAULT_FONT_SIZE,
9
9
  DEFAULT_CHARACTER_SPACING,
10
- DEFAULT_TOLERANCE,
11
- DEFAULT_FONT_SIZE_ADJUSTMENT,
12
- DEFAULT_PT_TO_MM_RATIO,
13
- DEFAULT_PT_TO_PX_RATIO,
10
+ DEFAULT_LINE_HEIGHT,
11
+ FONT_SIZE_ADJUSTMENT,
12
+ PT_TO_PX_RATIO,
13
+ DEFAULT_DYNAMIC_FIT,
14
+ DYNAMIC_FIT_HORIZONTAL,
15
+ DYNAMIC_FIT_VERTICAL,
14
16
  } from './constants';
17
+ import { mm2pt, pt2mm } from './helper';
15
18
  import { b64toUint8Array } from "."
16
19
 
17
20
  export const getFallbackFontName = (font: Font) => {
@@ -31,7 +34,7 @@ export const getFallbackFontName = (font: Font) => {
31
34
  const getFallbackFont = (font: Font) => {
32
35
  const fallbackFontName = getFallbackFontName(font);
33
36
  return font[fallbackFontName];
34
- }
37
+ };
35
38
 
36
39
  export const getDefaultFont = (): Font => ({
37
40
  [DEFAULT_FONT_NAME]: { data: b64toUint8Array(DEFAULT_FONT_VALUE), fallback: true },
@@ -77,7 +80,7 @@ export const checkFont = (arg: { font: Font; template: Template }) => {
77
80
  export const getFontAlignmentValue = (fontKitFont: FontKitFont, fontSize: number) => {
78
81
  const { ascent, descent, unitsPerEm } = fontKitFont;
79
82
 
80
- const fontSizeInPx = fontSize * DEFAULT_PT_TO_PX_RATIO;
83
+ const fontSizeInPx = fontSize * PT_TO_PX_RATIO;
81
84
 
82
85
  // Convert ascent and descent to px values
83
86
  const ascentInPixels = (ascent / unitsPerEm) * fontSizeInPx;
@@ -88,7 +91,7 @@ export const getFontAlignmentValue = (fontKitFont: FontKitFont, fontSize: number
88
91
 
89
92
  // Calculate the top margin/padding in px
90
93
  return ((singleLineHeight * fontSizeInPx) - fontSizeInPx) / 2
91
- }
94
+ };
92
95
 
93
96
  export const heightOfFontAtSize = (fontKitFont: FontKitFont, fontSize: number) => {
94
97
  const { ascent, descent, bbox, unitsPerEm } = fontKitFont;
@@ -103,46 +106,19 @@ export const heightOfFontAtSize = (fontKitFont: FontKitFont, fontSize: number) =
103
106
  return (height / 1000) * fontSize;
104
107
  };
105
108
 
106
- const widthOfTextAtSize = (input: string, fontKitFont: FontKitFont, fontSize: number) => {
107
- const { glyphs } = fontKitFont.layout(input);
108
- const scale = 1000 / fontKitFont.unitsPerEm;
109
- return glyphs.reduce((totalWidth, glyph) => totalWidth + glyph.advanceWidth * scale, 0) * (fontSize / 1000);
110
- }
111
-
112
- const calculateCharacterSpacing = (
113
- textContent: string,
114
- textCharacterSpacing: number
115
- ) => {
116
- const numberOfCharacters = textContent.length;
117
- return (numberOfCharacters - 1) * textCharacterSpacing;
109
+ const calculateCharacterSpacing = (textContent: string, textCharacterSpacing: number) => {
110
+ return (textContent.length - 1) * textCharacterSpacing;
118
111
  };
119
112
 
120
- const calculateTextWidthInMm = (textContent: string, textWidth: number, textCharacterSpacing: number) =>
121
- (textWidth + calculateCharacterSpacing(textContent, textCharacterSpacing)) * DEFAULT_PT_TO_MM_RATIO;
122
-
123
- const getLongestLine = (
124
- textContentRows: string[],
125
- fontKitFont: FontKitFont,
126
- fontSize: number,
127
- characterSpacingCount: number
128
- ) => {
129
- let longestLine = '';
130
- let maxLineWidth = 0;
131
-
132
- textContentRows.forEach((line) => {
133
- const textWidth = widthOfTextAtSize(line, fontKitFont, fontSize);
134
- const lineWidth = calculateTextWidthInMm(line, textWidth, characterSpacingCount);
135
-
136
- if (lineWidth > maxLineWidth) {
137
- longestLine = line;
138
- maxLineWidth = lineWidth;
139
- }
140
- });
141
-
142
- return longestLine;
113
+ export const widthOfTextAtSize = (text: string, fontKitFont: FontKitFont, fontSize: number, characterSpacing: number) => {
114
+ const { glyphs } = fontKitFont.layout(text);
115
+ const scale = 1000 / fontKitFont.unitsPerEm;
116
+ const standardWidth =
117
+ glyphs.reduce((totalWidth, glyph) => totalWidth + glyph.advanceWidth * scale, 0) *
118
+ (fontSize / 1000);
119
+ return standardWidth + calculateCharacterSpacing(text, characterSpacing);
143
120
  };
144
121
 
145
-
146
122
  const fontKitFontCache: { [fontName: string]: FontKitFont } = {};
147
123
  export const getFontKitFont = async (textSchema: TextSchema, font: Font) => {
148
124
  const fontName = textSchema.fontName || getFallbackFontName(font);
@@ -157,38 +133,194 @@ export const getFontKitFont = async (textSchema: TextSchema, font: Font) => {
157
133
  }
158
134
 
159
135
  const fontKitFont = fontkit.create(fontData instanceof Buffer ? fontData : Buffer.from(fontData as ArrayBuffer));
160
- fontKitFontCache[fontName] = fontKitFont
136
+ fontKitFontCache[fontName] = fontKitFont;
161
137
 
162
138
  return fontKitFont;
163
- }
139
+ };
164
140
 
165
- const getTextContent = (input: string, fontKitFont: FontKitFont, fontSize: number, characterSpacingCount: number): string => {
166
- const textContentRows = input.split('\n');
167
- return textContentRows.length > 1 ? getLongestLine(textContentRows, fontKitFont, fontSize, characterSpacingCount) : input;
168
- }
141
+ export type FontWidthCalcValues = {
142
+ font: FontKitFont;
143
+ fontSize: number;
144
+ characterSpacing: number;
145
+ boxWidthInPt: number;
146
+ };
147
+
148
+ const isTextExceedingBoxWidth = (text: string, calcValues: FontWidthCalcValues) => {
149
+ const { font, fontSize, characterSpacing, boxWidthInPt } = calcValues;
150
+ const textWidth = widthOfTextAtSize(text, font, fontSize, characterSpacing);
151
+ return textWidth > boxWidthInPt;
152
+ };
169
153
 
170
- export const calculateDynamicFontSize = async ({ textSchema, font, input }: { textSchema: TextSchema, font: Font, input: string }) => {
171
- const { fontSize: _fontSize, dynamicFontSize: dynamicFontSizeSetting, characterSpacing, width } = textSchema;
172
- const fontSize = _fontSize || DEFAULT_FONT_SIZE;
154
+ /**
155
+ * Incrementally checks the current line for its real length
156
+ * and returns the position where it exceeds the box width.
157
+ * Returns `null` to indicate if textLine is shorter than the available box.
158
+ */
159
+ const getOverPosition = (textLine: string, calcValues: FontWidthCalcValues) => {
160
+ for (let i = 0; i <= textLine.length; i++) {
161
+ if (isTextExceedingBoxWidth(textLine.slice(0, i + 1), calcValues)) {
162
+ return i;
163
+ }
164
+ }
165
+
166
+ return null;
167
+ };
168
+
169
+ /**
170
+ * Gets the position of the split. Splits the exceeding line at
171
+ * the last whitespace prior to it exceeding the bounding box width.
172
+ */
173
+ const getSplitPosition = (textLine: string, calcValues: FontWidthCalcValues) => {
174
+ const overPos = getOverPosition(textLine, calcValues);
175
+ if (overPos === null) return textLine.length; // input line is shorter than the available space
176
+
177
+ let overPosTmp = overPos;
178
+ while (textLine[overPosTmp] !== ' ' && overPosTmp >= 0) {
179
+ overPosTmp--;
180
+ }
181
+
182
+ // For very long lines with no whitespace use the original overPos
183
+ return overPosTmp > 0 ? overPosTmp : overPos;
184
+ };
185
+
186
+ /**
187
+ * Recursively splits the line at getSplitPosition.
188
+ * If there is some leftover, split the rest again in the same manner.
189
+ */
190
+ export const getSplittedLines = (textLine: string, calcValues: FontWidthCalcValues): string[] => {
191
+ const splitPos = getSplitPosition(textLine, calcValues);
192
+ const splittedLine = textLine.substring(0, splitPos);
193
+ const rest = textLine.substring(splitPos).trimStart();
194
+
195
+ if (rest === textLine) {
196
+ // if we went so small that we want to split on the first char
197
+ // then end recursion to avoid infinite loop
198
+ return [textLine];
199
+ }
200
+
201
+ if (rest.length === 0) {
202
+ // end recursion if there is no leftover
203
+ return [splittedLine];
204
+ }
205
+
206
+ return [splittedLine, ...getSplittedLines(rest, calcValues)];
207
+ };
208
+
209
+ /**
210
+ * If using dynamic font size, iteratively increment or decrement the
211
+ * font size to fit the containing box.
212
+ * Calculating space usage involves splitting lines where they exceed
213
+ * the box width based on the proposed size.
214
+ */
215
+ export const calculateDynamicFontSize = async ({
216
+ textSchema,
217
+ font,
218
+ input,
219
+ startingFontSize,
220
+ }: {
221
+ textSchema: TextSchema;
222
+ font: Font;
223
+ input: string;
224
+ startingFontSize?: number | undefined;
225
+ }) => {
226
+ const {
227
+ fontSize: schemaFontSize,
228
+ dynamicFontSize: dynamicFontSizeSetting,
229
+ characterSpacing: schemaCharacterSpacing,
230
+ width: boxWidth,
231
+ height: boxHeight,
232
+ lineHeight = DEFAULT_LINE_HEIGHT,
233
+ } = textSchema;
234
+ const fontSize = startingFontSize || schemaFontSize || DEFAULT_FONT_SIZE;
173
235
  if (!dynamicFontSizeSetting) return fontSize;
236
+ if (dynamicFontSizeSetting.max < dynamicFontSizeSetting.min) return fontSize;
174
237
 
175
- const characterSpacingCount = characterSpacing ?? DEFAULT_CHARACTER_SPACING;
238
+ const characterSpacing = schemaCharacterSpacing ?? DEFAULT_CHARACTER_SPACING;
176
239
  const fontKitFont = await getFontKitFont(textSchema, font);
177
- const textContent = getTextContent(input, fontKitFont, fontSize, characterSpacingCount);
178
- const textWidth = widthOfTextAtSize(textContent, fontKitFont, fontSize);
240
+ const textContentRows = input.split('\n');
179
241
 
180
242
  let dynamicFontSize = fontSize;
181
- let textWidthInMm = calculateTextWidthInMm(textContent, textWidth, characterSpacingCount);
243
+ if (dynamicFontSize < dynamicFontSizeSetting.min) {
244
+ dynamicFontSize = dynamicFontSizeSetting.min;
245
+ } else if (dynamicFontSize > dynamicFontSizeSetting.max) {
246
+ dynamicFontSize = dynamicFontSizeSetting.max;
247
+ }
248
+ const dynamicFontFit = dynamicFontSizeSetting.fit ?? DEFAULT_DYNAMIC_FIT;
249
+
250
+ const calculateConstraints = (size: number) => {
251
+ let totalWidthInMm = 0;
252
+ let totalHeightInMm = 0;
253
+
254
+ const boxWidthInPt = mm2pt(boxWidth);
255
+ const textHeight = heightOfFontAtSize(fontKitFont, size);
256
+ const textHeightInMm = pt2mm(textHeight * lineHeight);
257
+
258
+ textContentRows.forEach((paragraph) => {
259
+ const lines = getSplittedLines(paragraph, {
260
+ font: fontKitFont,
261
+ fontSize: size,
262
+ characterSpacing,
263
+ boxWidthInPt,
264
+ });
265
+ lines.forEach((line) => {
266
+ if (dynamicFontFit === DYNAMIC_FIT_VERTICAL) {
267
+ // For vertical fit we want to consider the width of text lines where we detect a split
268
+ const textWidth = widthOfTextAtSize(line, fontKitFont, size, characterSpacing);
269
+ const textWidthInMm = pt2mm(textWidth);
270
+ totalWidthInMm = Math.max(totalWidthInMm, textWidthInMm);
271
+ }
272
+
273
+ totalHeightInMm += textHeightInMm;
274
+ });
275
+ if (dynamicFontFit === DYNAMIC_FIT_HORIZONTAL) {
276
+ // For horizontal fit we want to consider the line's width 'unsplit'
277
+ const textWidth = widthOfTextAtSize(paragraph, fontKitFont, size, characterSpacing);
278
+ const textWidthInMm = pt2mm(textWidth);
279
+ totalWidthInMm = Math.max(totalWidthInMm, textWidthInMm);
280
+ }
281
+ });
282
+
283
+ return { totalWidthInMm, totalHeightInMm };
284
+ };
285
+
286
+ const shouldFontGrowToFit = (totalWidthInMm: number, totalHeightInMm: number) => {
287
+ if (dynamicFontSize >= dynamicFontSizeSetting.max) {
288
+ return false;
289
+ }
290
+ if (dynamicFontFit === DYNAMIC_FIT_HORIZONTAL) {
291
+ return totalWidthInMm < boxWidth;
292
+ }
293
+ return totalHeightInMm < boxHeight;
294
+ };
182
295
 
183
- while (textWidthInMm > width - DEFAULT_TOLERANCE && dynamicFontSize > dynamicFontSizeSetting.min) {
184
- dynamicFontSize -= DEFAULT_FONT_SIZE_ADJUSTMENT;
185
- textWidthInMm = calculateTextWidthInMm(textContent, widthOfTextAtSize(textContent, fontKitFont, dynamicFontSize), characterSpacingCount);
296
+ const shouldFontShrinkToFit = (totalWidthInMm: number, totalHeightInMm: number) => {
297
+ if (dynamicFontSize <= dynamicFontSizeSetting.min || dynamicFontSize <= 0) {
298
+ return false;
299
+ }
300
+ return totalWidthInMm > boxWidth || totalHeightInMm > boxHeight;
301
+ };
302
+
303
+ let { totalWidthInMm, totalHeightInMm } = calculateConstraints(dynamicFontSize);
304
+
305
+ // Attempt to increase the font size up to desired fit
306
+ while (shouldFontGrowToFit(totalWidthInMm, totalHeightInMm)) {
307
+ dynamicFontSize += FONT_SIZE_ADJUSTMENT;
308
+ const { totalWidthInMm: newWidth, totalHeightInMm: newHeight } = calculateConstraints(dynamicFontSize);
309
+
310
+ if (newHeight < boxHeight) {
311
+ totalWidthInMm = newWidth;
312
+ totalHeightInMm = newHeight;
313
+ } else {
314
+ dynamicFontSize -= FONT_SIZE_ADJUSTMENT;
315
+ break;
316
+ }
186
317
  }
187
318
 
188
- while (textWidthInMm < width - DEFAULT_TOLERANCE && dynamicFontSize < dynamicFontSizeSetting.max) {
189
- dynamicFontSize += DEFAULT_FONT_SIZE_ADJUSTMENT;
190
- textWidthInMm = calculateTextWidthInMm(textContent, widthOfTextAtSize(textContent, fontKitFont, dynamicFontSize), characterSpacingCount);
319
+ // Attempt to decrease the font size down to desired fit
320
+ while (shouldFontShrinkToFit(totalWidthInMm, totalHeightInMm)) {
321
+ dynamicFontSize -= FONT_SIZE_ADJUSTMENT;
322
+ ({ totalWidthInMm, totalHeightInMm } = calculateConstraints(dynamicFontSize));
191
323
  }
192
324
 
193
325
  return dynamicFontSize;
194
- };
326
+ };
package/src/helper.ts CHANGED
@@ -10,7 +10,16 @@ import {
10
10
  GenerateProps as GeneratePropsSchema,
11
11
  UIProps as UIPropsSchema,
12
12
  } from './schema';
13
- import { checkFont } from "./font"
13
+ import { MM_TO_PT_RATIO, PT_TO_MM_RATIO } from './constants';
14
+ import { checkFont } from './font';
15
+
16
+ export const mm2pt = (mm: number): number => {
17
+ return parseFloat(String(mm)) * MM_TO_PT_RATIO;
18
+ };
19
+
20
+ export const pt2mm = (pt: number): number => {
21
+ return pt * PT_TO_MM_RATIO;
22
+ };
14
23
 
15
24
  const blob2Base64Pdf = (blob: Blob) => {
16
25
  return new Promise<string>((resolve, reject) => {
package/src/index.ts CHANGED
@@ -5,10 +5,13 @@ import {
5
5
  DEFAULT_LINE_HEIGHT,
6
6
  DEFAULT_CHARACTER_SPACING,
7
7
  DEFAULT_FONT_COLOR,
8
- DEFAULT_TOLERANCE,
9
- DEFAULT_FONT_SIZE_ADJUSTMENT,
10
- DEFAULT_PT_TO_MM_RATIO,
11
- DEFAULT_PT_TO_PX_RATIO,
8
+ DYNAMIC_FIT_VERTICAL,
9
+ DYNAMIC_FIT_HORIZONTAL,
10
+ DEFAULT_DYNAMIC_FIT,
11
+ FONT_SIZE_ADJUSTMENT,
12
+ MM_TO_PT_RATIO,
13
+ PT_TO_MM_RATIO,
14
+ PT_TO_PX_RATIO,
12
15
  BLANK_PDF,
13
16
  DEFAULT_FONT_VALUE,
14
17
  } from './constants.js';
@@ -48,15 +51,20 @@ import {
48
51
  checkPreviewProps,
49
52
  checkDesignerProps,
50
53
  checkGenerateProps,
54
+ mm2pt,
51
55
  validateBarcodeInput,
52
56
  } from './helper.js';
53
57
  import {
54
- calculateDynamicFontSize, getFallbackFontName,
58
+ calculateDynamicFontSize,
59
+ getFallbackFontName,
55
60
  getDefaultFont,
56
61
  heightOfFontAtSize,
62
+ widthOfTextAtSize,
57
63
  checkFont,
58
64
  getFontKitFont,
59
65
  getFontAlignmentValue,
66
+ getSplittedLines,
67
+ FontWidthCalcValues,
60
68
  } from './font.js';
61
69
 
62
70
  export {
@@ -66,10 +74,13 @@ export {
66
74
  DEFAULT_LINE_HEIGHT,
67
75
  DEFAULT_CHARACTER_SPACING,
68
76
  DEFAULT_FONT_COLOR,
69
- DEFAULT_TOLERANCE,
70
- DEFAULT_FONT_SIZE_ADJUSTMENT,
71
- DEFAULT_PT_TO_MM_RATIO,
72
- DEFAULT_PT_TO_PX_RATIO,
77
+ DYNAMIC_FIT_VERTICAL,
78
+ DYNAMIC_FIT_HORIZONTAL,
79
+ DEFAULT_DYNAMIC_FIT,
80
+ FONT_SIZE_ADJUSTMENT,
81
+ MM_TO_PT_RATIO,
82
+ PT_TO_MM_RATIO,
83
+ PT_TO_PX_RATIO,
73
84
  BLANK_PDF,
74
85
  DEFAULT_FONT_VALUE,
75
86
  schemaTypes,
@@ -80,7 +91,10 @@ export {
80
91
  b64toUint8Array,
81
92
  getFallbackFontName,
82
93
  getDefaultFont,
94
+ getSplittedLines,
83
95
  heightOfFontAtSize,
96
+ widthOfTextAtSize,
97
+ mm2pt,
84
98
  checkFont,
85
99
  getFontKitFont,
86
100
  getFontAlignmentValue,
@@ -119,4 +133,5 @@ export type {
119
133
  PreviewReactProps,
120
134
  DesignerProps,
121
135
  DesignerReactProps,
136
+ FontWidthCalcValues,
122
137
  };
package/src/schema.ts CHANGED
@@ -37,6 +37,7 @@ export const TextSchema = CommonSchema.extend({
37
37
  dynamicFontSize: z.object({
38
38
  max: z.number(),
39
39
  min: z.number(),
40
+ fit: z.string().optional(),
40
41
  }).optional(),
41
42
  });
42
43