Repository: Hopding/pdf-lib
Pull request #776 (2021-01-27 00:18:05)

Cleanup 754a


Patch:
diff --git a/src/api/form/PDFButton.ts b/src/api/form/PDFButton.ts
index 1bb494f..e248009 100644
--- a/src/api/form/PDFButton.ts
+++ b/src/api/form/PDFButton.ts
@@ -21,7 +21,11 @@ import {
   PDFAcroPushButton,
   PDFWidgetAnnotation,
 } from 'src/core';
-import { assertIs, assertOrUndefined } from 'src/utils';
+import {
+  assertIs,
+  assertOrUndefined,
+  assertPositiveNum,
+} from 'src/utils';
 
 /**
  * Represents a button field of a [[PDFForm]].
@@ -237,4 +241,19 @@ export default class PDFButton extends PDFField {
     const appearances = normalizeAppearance(apProvider(this, widget, font));
     this.updateWidgetAppearanceWithFont(widget, font, appearances);
   }
+
+  /**
+   * Set the font size for the text in this field. There needs to be a
+   * default appearance string (DA) set with a font value specified
+   * for this to work. For example:
+   * ```js
+   * const button = form.getButton('some.button.field')
+   * button.setFontSize(4);
+   * ```
+   * @param fontSize The font size to set the font to.
+   */
+  setFontSize(fontSize: number) {
+    assertPositiveNum(fontSize, 'fontSize');
+    this.acroField.setFontSize(fontSize);
+  }
 }
diff --git a/src/api/form/PDFDropdown.ts b/src/api/form/PDFDropdown.ts
index 5f1964e..83b6ecb 100644
--- a/src/api/form/PDFDropdown.ts
+++ b/src/api/form/PDFDropdown.ts
@@ -22,7 +22,11 @@ import {
   PDFAcroComboBox,
   AcroChoiceFlags,
 } from 'src/core';
-import { assertIs, assertOrUndefined } from 'src/utils';
+import {
+  assertIs,
+  assertOrUndefined,
+  assertPositiveNum,
+} from 'src/utils';
 
 /**
  * Represents a dropdown field of a [[PDFForm]].
@@ -622,4 +626,19 @@ export default class PDFDropdown extends PDFField {
     const appearances = normalizeAppearance(apProvider(this, widget, font));
     this.updateWidgetAppearanceWithFont(widget, font, appearances);
   }
+
+  /**
+   * Set the font size for the text in this field. There needs to be a
+   * default appearance string (DA) set with a font value specified
+   * for this to work. For example:
+   * ```js
+   * const dropdown = form.getDropdown('some.dropdown.field')
+   * dropdown.setFontSize(4);
+   * ```
+   * @param fontSize The font size to set the font to.
+   */
+  setFontSize(fontSize: number) {
+    assertPositiveNum(fontSize, 'fontSize');
+    this.acroField.setFontSize(fontSize);
+  }
 }
diff --git a/src/api/form/PDFOptionList.ts b/src/api/form/PDFOptionList.ts
index 4adf861..e555025 100644
--- a/src/api/form/PDFOptionList.ts
+++ b/src/api/form/PDFOptionList.ts
@@ -22,7 +22,12 @@ import {
   AcroChoiceFlags,
   PDFWidgetAnnotation,
 } from 'src/core';
-import { assertIs, assertOrUndefined, assertIsSubset } from 'src/utils';
+import {
+  assertIs,
+  assertIsSubset,
+  assertOrUndefined,
+  assertPositiveNum,
+} from 'src/utils';
 
 /**
  * Represents an option list field of a [[PDFForm]].
@@ -525,4 +530,19 @@ export default class PDFOptionList extends PDFField {
     const appearances = normalizeAppearance(apProvider(this, widget, font));
     this.updateWidgetAppearanceWithFont(widget, font, appearances);
   }
+
+  /**
+   * Set the font size for the text in this field. There needs to be a
+   * default appearance string (DA) set with a font value specified
+   * for this to work. For example:
+   * ```js
+   * const optionList = form.getOptionList('some.optionList.field')
+   * optionList.setFontSize(4);
+   * ```
+   * @param fontSize The font size to set the font to.
+   */
+  setFontSize(fontSize: number) {
+    assertPositiveNum(fontSize, 'fontSize');
+    this.acroField.setFontSize(fontSize);
+  }
 }
diff --git a/src/api/form/PDFTextField.ts b/src/api/form/PDFTextField.ts
index 1a9901b..3300b34 100644
--- a/src/api/form/PDFTextField.ts
+++ b/src/api/form/PDFTextField.ts
@@ -33,6 +33,7 @@ import {
   assertIs,
   assertIsOneOf,
   assertOrUndefined,
+  assertPositiveNum,
   assertRangeOrUndefined,
 } from 'src/utils';
 
@@ -795,4 +796,19 @@ export default class PDFTextField extends PDFField {
     const appearances = normalizeAppearance(apProvider(this, widget, font));
     this.updateWidgetAppearanceWithFont(widget, font, appearances);
   }
+
+  /**
+   * Set the font size for the text in this field. There needs to be a
+   * default appearance string (DA) set with a font value specified
+   * for this to work. For example:
+   * ```js
+   * const filingPartner = form.getTextField(fieldNames.FilingPartner);
+   * filingPartner.setFontSize(4);
+   * ```
+   * @param fontSize The font size to set the font to.
+   */
+  setFontSize(fontSize: number) {
+    assertPositiveNum(fontSize, 'fontSize');
+    this.acroField.setFontSize(fontSize);
+  }
 }
diff --git a/src/api/form/appearances.ts b/src/api/form/appearances.ts
index 3252320..1bbbf55 100644
--- a/src/api/form/appearances.ts
+++ b/src/api/form/appearances.ts
@@ -33,6 +33,7 @@ import {
 } from 'src/api/text/layout';
 import { TextAlignment } from 'src/api/text/alignment';
 import { setFontAndSize } from 'src/api/operators';
+import { findLastMatch } from 'src/utils';
 
 /*********************** Appearance Provider Types ****************************/
 
@@ -108,30 +109,21 @@ export const normalizeAppearance = <T>(
   return { normal: appearance };
 };
 
-const findLastMatch = (value: string, regex: RegExp) => {
-  let position = 0;
-  let lastMatch: RegExpMatchArray | undefined;
-  while (position < value.length) {
-    const match = value.substring(position).match(regex);
-    if (!match) return lastMatch;
-    lastMatch = match;
-    position += (match.index ?? 0) + match[0].length;
-  }
-  return lastMatch;
-};
-
 // Examples:
-//   `/Helv 12 Tf` -> ['Helv', '12']
-//   `/HeBo 8.00 Tf` -> ['HeBo', '8.00']
+//   `/Helv 12 Tf` -> ['/Helv 12 Tf', 'Helv', '12']
+//   `/HeBo 8.00 Tf` -> ['/HeBo 8 Tf', 'HeBo', '8.00']
 const tfRegex = /\/([^\0\t\n\f\r\ ]+)[\0\t\n\f\r\ ]+(\d*\.\d+|\d+)[\0\t\n\f\r\ ]+Tf/;
 
 const getDefaultFontSize = (field: {
   getDefaultAppearance(): string | undefined;
 }) => {
   const da = field.getDefaultAppearance() ?? '';
-  const daMatch = findLastMatch(da, tfRegex) ?? [];
-  const defaultFontSize = Number(daMatch[2]);
-  return isFinite(defaultFontSize) ? defaultFontSize : undefined;
+  const daMatch = findLastMatch(da, tfRegex);
+  if (daMatch.match) {
+    return Number(daMatch.match[2]);
+  } else {
+    return undefined;
+  }
 };
 
 // Examples:
@@ -146,7 +138,7 @@ const getDefaultColor = (field: {
   const da = field.getDefaultAppearance() ?? '';
   const daMatch = findLastMatch(da, colorRegex);
 
-  const [, c1, c2, c3, c4, colorSpace] = daMatch ?? [];
+  const [, c1, c2, c3, c4, colorSpace] = daMatch.match ?? [];
 
   if (colorSpace === 'g' && c1) {
     return grayscale(Number(c1));
diff --git a/src/core/acroform/PDFAcroField.ts b/src/core/acroform/PDFAcroField.ts
index 716aba0..7fd16fc 100644
--- a/src/core/acroform/PDFAcroField.ts
+++ b/src/core/acroform/PDFAcroField.ts
@@ -6,6 +6,13 @@ import PDFObject from 'src/core/objects/PDFObject';
 import PDFNumber from 'src/core/objects/PDFNumber';
 import PDFArray from 'src/core/objects/PDFArray';
 import PDFRef from 'src/core/objects/PDFRef';
+import { findLastMatch } from 'src/utils';
+
+// Examples:
+//   `/Helv 12 Tf` -> ['Helv', '12']
+//   `/HeBo 8.00 Tf` -> ['HeBo', '8.00']
+//   `/HeBo Tf` -> ['HeBo', undefined]
+const tfRegex = /\/([^\0\t\n\f\r\ ]+)[\0\t\n\f\r\ ]*(\d*\.\d+|\d+)?[\0\t\n\f\r\ ]+Tf/;
 
 class PDFAcroField {
   readonly dict: PDFDict;
@@ -96,6 +103,24 @@ class PDFAcroField {
     return DA?.asString();
   }
 
+  setFontSize(fontSize: number) {
+    const da = this.getDefaultAppearance();
+    if (da) {
+      const daMatch = findLastMatch(da, tfRegex);
+      if (daMatch.match && Number(daMatch.match[2]) !== fontSize) {
+        let daEnd = '';
+        if (daMatch.pos <= da.length) {
+          daEnd = da.slice(daMatch.pos);
+        }
+        this.setDefaultAppearance(
+          `${da.slice(0, daMatch.pos - daMatch.match[0].length)}/${
+            daMatch.match[1]
+          } ${fontSize} Tf${daEnd}`,
+        );
+      }
+    }
+  }
+
   getFlags(): number {
     return this.Ff()?.asNumber() ?? 0;
   }
diff --git a/src/utils/strings.ts b/src/utils/strings.ts
index 9c21614..d4af691 100644
--- a/src/utils/strings.ts
+++ b/src/utils/strings.ts
@@ -168,3 +168,15 @@ export const parseDate = (dateStr: string): Date | undefined => {
 
   return date;
 };
+
+export const findLastMatch = (value: string, regex: RegExp) => {
+  let position = 0;
+  let lastMatch: RegExpMatchArray | undefined;
+  while (position < value.length) {
+    const match = value.substring(position).match(regex);
+    if (!match) return { match: lastMatch, pos: position };
+    lastMatch = match;
+    position += (match.index ?? 0) + match[0].length;
+  }
+  return { match: lastMatch, pos: position };
+};
\ No newline at end of file
diff --git a/src/utils/validators.ts b/src/utils/validators.ts
index 739eaee..5a404c6 100644
--- a/src/utils/validators.ts
+++ b/src/utils/validators.ts
@@ -215,7 +215,16 @@ export const assertMultiple = (
 export const assertInteger = (value: any, valueName: string) => {
   if (!Number.isInteger(value)) {
     throw new Error(
-      `${backtick(valueName)} msut be an integer, but was actually ${value}`,
+      `${backtick(valueName)} must be an integer, but was actually ${value}`,
     );
   }
 };
+
+export const assertPositiveNum = (
+  value: number,
+  valueName: string
+) => {
+  if (![1, 0].includes(Math.sign(value))) {
+    throw new Error(`${backtick(valueName)} must be a positive number or 0, but was actually ${value}`);
+  }
+};
\ No newline at end of file


Test patch:
diff --git a/apps/deno/tests/test16.ts b/apps/deno/tests/test16.ts
index 2559fe7..aa10e6e 100644
--- a/apps/deno/tests/test16.ts
+++ b/apps/deno/tests/test16.ts
@@ -199,12 +199,15 @@ export default async (assets: Assets) => {
 
   const filingPartner = form.getTextField(fieldNames.FilingPartner);
   filingPartner.setText('Guinevere Pendragon');
+  filingPartner.setFontSize(4);
 
   const firstName = form.getTextField(fieldNames.FirstNameAndMiddleInitial);
   firstName.setText('Arthur F');
+  firstName.setFontSize(20);
 
   const lastName = form.getTextField(fieldNames.LastName);
   lastName.setText('Pendragon');
+  lastName.setFontSize(30);
 
   const ssn = form.getTextField(fieldNames.SSN);
   ssn.setText('123456789');
diff --git a/apps/deno/tests/test17.ts b/apps/deno/tests/test17.ts
index 10c9b84..dab650a 100644
--- a/apps/deno/tests/test17.ts
+++ b/apps/deno/tests/test17.ts
@@ -87,6 +87,7 @@ export default async (assets: Assets) => {
   const ol = form.createOptionList('custom.optionlist.field');
   const rg = form.createRadioGroup('custom.radiogroup.field');
   const tf = form.createTextField('custom.text.field');
+  const tfFontSize = form.createTextField('custom.text.fieldFontSize');
 
   dd.addOptions('∑');
   ol.addOptions('∑');
@@ -108,6 +109,10 @@ export default async (assets: Assets) => {
   rg.addOptionToPage('bar', page, { x, y, width, height });
   y -= height + 25;
   tf.addToPage(page, { x, y, width, height, font: symbol });
+  y -= height + 25;
+  tfFontSize.addToPage(page, { x, y, width, height, font: ubuntuFont });
+  tfFontSize.setFontSize(80);
+  tfFontSize.setText('This text should be huge');
 
   const rectangle = drawRectangle({
     x: 0,
diff --git a/apps/node/tests/test16.ts b/apps/node/tests/test16.ts
index 3538877..be63154 100644
--- a/apps/node/tests/test16.ts
+++ b/apps/node/tests/test16.ts
@@ -197,12 +197,15 @@ export default async (assets: Assets) => {
 
   const filingPartner = form.getTextField(fieldNames.FilingPartner);
   filingPartner.setText('Guinevere Pendragon');
+  filingPartner.setFontSize(4);
 
   const firstName = form.getTextField(fieldNames.FirstNameAndMiddleInitial);
   firstName.setText('Arthur F');
+  firstName.setFontSize(20);
 
   const lastName = form.getTextField(fieldNames.LastName);
   lastName.setText('Pendragon');
+  lastName.setFontSize(30);
 
   const ssn = form.getTextField(fieldNames.SSN);
   ssn.setText('123456789');
diff --git a/apps/node/tests/test17.ts b/apps/node/tests/test17.ts
index 828416b..dd15537 100644
--- a/apps/node/tests/test17.ts
+++ b/apps/node/tests/test17.ts
@@ -85,6 +85,7 @@ export default async (assets: Assets) => {
   const ol = form.createOptionList('custom.optionlist.field');
   const rg = form.createRadioGroup('custom.radiogroup.field');
   const tf = form.createTextField('custom.text.field');
+  const tfFontSize = form.createTextField('custom.text.fieldFontSize');
 
   dd.addOptions('∑');
   ol.addOptions('∑');
@@ -106,6 +107,10 @@ export default async (assets: Assets) => {
   rg.addOptionToPage('bar', page, { x, y, width, height });
   y -= height + 25;
   tf.addToPage(page, { x, y, width, height, font: symbol });
+  y -= height + 25;
+  tfFontSize.addToPage(page, { x, y, width, height, font: ubuntuFont });
+  tfFontSize.setFontSize(80);
+  tfFontSize.setText('This text should be huge');
 
   const rectangle = drawRectangle({
     x: 0,
diff --git a/apps/rn/src/tests/test16.js b/apps/rn/src/tests/test16.js
index 6beaad8..37b9e75 100644
--- a/apps/rn/src/tests/test16.js
+++ b/apps/rn/src/tests/test16.js
@@ -200,12 +200,15 @@ export default async () => {
 
   const filingPartner = form.getTextField(fieldNames.FilingPartner);
   filingPartner.setText('Guinevere Pendragon');
+  filingPartner.setFontSize(4);
 
   const firstName = form.getTextField(fieldNames.FirstNameAndMiddleInitial);
   firstName.setText('Arthur F');
+  firstName.setFontSize(20); 
 
   const lastName = form.getTextField(fieldNames.LastName);
   lastName.setText('Pendragon');
+  lastName.setFontSize(30);
 
   const ssn = form.getTextField(fieldNames.SSN);
   ssn.setText('123456789');
diff --git a/apps/rn/src/tests/test17.js b/apps/rn/src/tests/test17.js
index 3956266..da99e1d 100644
--- a/apps/rn/src/tests/test17.js
+++ b/apps/rn/src/tests/test17.js
@@ -90,6 +90,7 @@ export default async () => {
   const ol = form.createOptionList('custom.optionlist.field');
   const rg = form.createRadioGroup('custom.radiogroup.field');
   const tf = form.createTextField('custom.text.field');
+  const tfFontSize = form.createTextField('custom.text.fieldFontSize');
 
   dd.addOptions('∑');
   ol.addOptions('∑');
@@ -111,6 +112,10 @@ export default async () => {
   rg.addOptionToPage('bar', page, { x, y, width, height });
   y -= height + 25;
   tf.addToPage(page, { x, y, width, height, font: symbol });
+  y -= height + 25;
+  tfFontSize.addToPage(page, { x, y, width, height, font: ubuntuFont });
+  tfFontSize.setFontSize(80);
+  tfFontSize.setText('This text should be huge');
 
   const rectangle = drawRectangle({
     x: 0,
diff --git a/apps/web/test16.html b/apps/web/test16.html
index 2d9a802..1a4b57f 100644
--- a/apps/web/test16.html
+++ b/apps/web/test16.html
@@ -254,12 +254,15 @@
 
       const filingPartner = form.getTextField(fieldNames.FilingPartner);
       filingPartner.setText('Guinevere Pendragon');
+      filingPartner.setFontSize(4);
 
       const firstName = form.getTextField(fieldNames.FirstNameAndMiddleInitial);
       firstName.setText('Arthur F');
+      firstName.setFontSize(20);
 
       const lastName = form.getTextField(fieldNames.LastName);
       lastName.setText('Pendragon');
+      lastName.setFontSize(30);
 
       const ssn = form.getTextField(fieldNames.SSN);
       ssn.setText('123456789');
diff --git a/apps/web/test17.html b/apps/web/test17.html
index 2902c05..e51b737 100644
--- a/apps/web/test17.html
+++ b/apps/web/test17.html
@@ -139,6 +139,7 @@
       const ol = form.createOptionList('custom.optionlist.field');
       const rg = form.createRadioGroup('custom.radiogroup.field');
       const tf = form.createTextField('custom.text.field');
+      const tfFontSize = form.createTextField('custom.text.fieldFontSize');
 
       dd.addOptions('∑');
       ol.addOptions('∑');
@@ -160,6 +161,10 @@
       rg.addOptionToPage('bar', page, { x, y, width, height });
       y -= height + 25;
       tf.addToPage(page, { x, y, width, height, font: symbol });
+      y -= height + 25;
+      tfFontSize.addToPage(page, { x, y, width, height, font: ubuntuFont });
+      tfFontSize.setFontSize(80);
+      tfFontSize.setText('This text should be huge');
 
       const rectangle = drawRectangle({
         x: 0,
diff --git a/tests/core/acroform/PDFAcroField.spec.ts b/tests/core/acroform/PDFAcroField.spec.ts
index 5ff108b..83220f7 100644
--- a/tests/core/acroform/PDFAcroField.spec.ts
+++ b/tests/core/acroform/PDFAcroField.spec.ts
@@ -41,4 +41,30 @@ describe(`PDFAcroField`, () => {
 
     expect(field.getDefaultAppearance()).toBe('/ZaDb 10 Tf 0 g');
   });
+
+  it(`returns new font size direct appearance strings (DAs) (1)`, () => {
+    const context = PDFContext.create();
+
+    const dict = context.obj({
+      DA: PDFString.of('/ZaDb 10 Tf 0 g'),
+    });
+    const dictRef = context.register(dict);
+    const field = PDFAcroTerminal.fromDict(dict, dictRef);
+    field.setFontSize(8);
+
+    expect(field.getDefaultAppearance()).toBe('/ZaDb 8 Tf 0 g');
+  });
+
+  it(`returns new font size direct appearance strings (DAs) (2)`, () => {
+    const context = PDFContext.create();
+
+    const dict = context.obj({
+      DA: PDFString.of('/ZaDb Tf 0 g'),
+    });
+    const dictRef = context.register(dict);
+    const field = PDFAcroTerminal.fromDict(dict, dictRef);
+    field.setFontSize(8);
+
+    expect(field.getDefaultAppearance()).toBe('/ZaDb 8 Tf 0 g');
+  });
 });