@signiphi/page-assembly 0.2.0-beta.17 → 0.2.0-beta.19

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/index.mjs CHANGED
@@ -14,7 +14,7 @@ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
14
14
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
15
15
  import * as TooltipPrimitive from '@radix-ui/react-tooltip';
16
16
  import * as pdfjsLib from 'pdfjs-dist';
17
- import { PDFDocument, PDFName, PDFTextField, PDFCheckBox, PDFDropdown, PDFRadioGroup, PDFHexString, StandardFonts, rgb, PDFString, PDFOperator, PDFOperatorNames, PDFDict, PDFArray, PDFInvalidObject, PDFObjectParser, PDFRawStream, PDFObjectStreamParser, PDFRef, PDFBool, PDFNumber } from 'pdf-lib';
17
+ import { PDFDocument, PDFTextField, PDFCheckBox, PDFDropdown, PDFRadioGroup, PDFHexString, PDFName, StandardFonts, rgb, PDFString, PDFOperator, PDFOperatorNames, PDFDict, PDFArray, PDFInvalidObject, PDFObjectParser, PDFRawStream, PDFObjectStreamParser, PDFRef, PDFBool, PDFNumber } from 'pdf-lib';
18
18
 
19
19
  // src/components/PageAssembler.tsx
20
20
  function cn(...inputs) {
@@ -316,16 +316,23 @@ var AlertDialogOverlay = React7.forwardRef(({ className, ...props }, ref) => /*
316
316
  }
317
317
  ));
318
318
  AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
319
- var AlertDialogContent = React7.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsxs(AlertDialogPortal, { container: document.querySelector(".signiphi-page-assembly"), children: [
319
+ var CENTERED = {
320
+ position: "fixed",
321
+ left: "50%",
322
+ top: "50%",
323
+ transform: "translate(-50%, -50%)"
324
+ };
325
+ var AlertDialogContent = React7.forwardRef(({ className, style, ...props }, ref) => /* @__PURE__ */ jsxs(AlertDialogPortal, { container: document.querySelector(".signiphi-page-assembly"), children: [
320
326
  /* @__PURE__ */ jsx(AlertDialogOverlay, {}),
321
327
  /* @__PURE__ */ jsx(
322
328
  AlertDialogPrimitive.Content,
323
329
  {
324
330
  ref,
325
331
  className: cn(
326
- "fixed left-[50%] top-[50%] z-[9999] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
332
+ "z-[9999] grid w-full max-w-lg gap-4 border bg-background p-6 shadow-lg sm:rounded-lg",
327
333
  className
328
334
  ),
335
+ style: { ...CENTERED, ...style },
329
336
  ...props
330
337
  }
331
338
  )
@@ -1374,6 +1381,46 @@ function downloadPdf(pdfBytes, filename) {
1374
1381
  a.click();
1375
1382
  URL.revokeObjectURL(url);
1376
1383
  }
1384
+ var KEY = "SigniphiMetadata";
1385
+ function readMetadata(doc, pdfLib) {
1386
+ let blockPresent = false;
1387
+ try {
1388
+ const infoRef = doc.context.trailerInfo.Info;
1389
+ if (!infoRef) return { metadata: null, blockPresent };
1390
+ const info = doc.context.lookup(infoRef);
1391
+ if (!info || typeof info.get !== "function") return { metadata: null, blockPresent };
1392
+ const value = info.get(pdfLib.PDFName.of(KEY));
1393
+ if (!value) return { metadata: null, blockPresent };
1394
+ blockPresent = true;
1395
+ const json = typeof value.decodeText === "function" ? value.decodeText() : stripDelimiters(value.toString());
1396
+ return { metadata: JSON.parse(json), blockPresent };
1397
+ } catch {
1398
+ return { metadata: null, blockPresent };
1399
+ }
1400
+ }
1401
+ function writeMetadata(doc, metadata, pdfLib) {
1402
+ const info = doc.context.lookup(doc.context.trailerInfo.Info);
1403
+ if (!info || typeof info.set !== "function") {
1404
+ throw new Error("Cannot write SigniphiMetadata: the PDF has no Info dictionary");
1405
+ }
1406
+ info.set(pdfLib.PDFName.of(KEY), pdfLib.PDFHexString.fromText(JSON.stringify(metadata)));
1407
+ }
1408
+ function stripDelimiters(raw) {
1409
+ const wrapped = raw.startsWith("(") && raw.endsWith(")") || raw.startsWith("<") && raw.endsWith(">");
1410
+ return wrapped ? raw.slice(1, -1) : raw;
1411
+ }
1412
+ function textLabelPlacements(source) {
1413
+ const fields = source.metadata?.fields;
1414
+ if (!fields || !source.pages?.length) return [];
1415
+ const out = [];
1416
+ for (const [name, entry] of Object.entries(fields)) {
1417
+ if (entry.type !== "text_label" || !entry.position) continue;
1418
+ for (const { source: page, combined } of source.pages) {
1419
+ if (page === entry.position.page) out.push({ name, page: combined });
1420
+ }
1421
+ }
1422
+ return out;
1423
+ }
1377
1424
  function toCombinedPage(entry, page) {
1378
1425
  if (!entry.position && !entry.radioPositions && !entry.radioOptions) return entry;
1379
1426
  const carried = { ...entry };
@@ -1384,78 +1431,55 @@ function toCombinedPage(entry, page) {
1384
1431
  }
1385
1432
  return carried;
1386
1433
  }
1387
- function mergeSourceMetadata(sources) {
1434
+ function assembleMetadata(sources) {
1388
1435
  const merged = { version: "1.0", fields: {}, fieldIdIndex: {} };
1389
1436
  let radioLabelsBaked;
1390
1437
  let textLabelsBaked;
1391
1438
  let checkboxLabelsBaked;
1439
+ let legacyRadioStripped;
1392
1440
  for (const source of sources) {
1393
1441
  const meta = source.metadata;
1394
- if (!meta || source.fields.length === 0) continue;
1442
+ if (!meta || source.fields.length === 0 && !source.pages?.length) continue;
1395
1443
  if (meta.radioLabelsBaked === false) radioLabelsBaked = false;
1396
1444
  else if (meta.radioLabelsBaked === true && radioLabelsBaked === void 0) radioLabelsBaked = true;
1397
1445
  if (meta.textLabelsBaked === false) textLabelsBaked = false;
1398
1446
  else if (meta.textLabelsBaked === true && textLabelsBaked === void 0) textLabelsBaked = true;
1399
1447
  if (meta.checkboxLabelsBaked === false) checkboxLabelsBaked = false;
1400
1448
  else if (meta.checkboxLabelsBaked === true && checkboxLabelsBaked === void 0) checkboxLabelsBaked = true;
1449
+ legacyRadioStripped = meta.legacyRadioStripped === true && legacyRadioStripped !== false;
1401
1450
  }
1402
1451
  for (const source of sources) {
1403
1452
  const meta = source.metadata;
1404
1453
  if (!meta) continue;
1405
1454
  const sourceTextLabelsBaked = meta.textLabelsBaked !== false;
1406
- for (const { name, page } of source.fields) {
1455
+ for (const { name, page } of [...source.fields, ...textLabelPlacements(source)]) {
1407
1456
  const key = `${name}_p${page}`;
1408
1457
  const entry = meta.fields?.[name];
1409
- if (entry) {
1410
- const carried = toCombinedPage(entry, page);
1411
- merged.fields[key] = textLabelsBaked === false && sourceTextLabelsBaked ? { ...carried, textLabelBaked: true } : carried;
1412
- }
1413
- const fieldId = meta.fieldIdIndex?.[name];
1414
- if (fieldId) merged.fieldIdIndex[key] = fieldId;
1458
+ if (!entry) continue;
1459
+ const carried = toCombinedPage(entry, page);
1460
+ merged.fields[key] = textLabelsBaked === false && sourceTextLabelsBaked ? { ...carried, textLabelBaked: true } : carried;
1461
+ if (entry.fieldId) merged.fieldIdIndex[entry.fieldId] = key;
1415
1462
  }
1416
1463
  }
1417
1464
  if (radioLabelsBaked !== void 0) merged.radioLabelsBaked = radioLabelsBaked;
1418
1465
  if (textLabelsBaked !== void 0) merged.textLabelsBaked = textLabelsBaked;
1419
1466
  if (checkboxLabelsBaked !== void 0) merged.checkboxLabelsBaked = checkboxLabelsBaked;
1467
+ if (legacyRadioStripped) merged.legacyRadioStripped = true;
1420
1468
  const hasEntries = Object.keys(merged.fields).length > 0;
1421
1469
  const hasMarkers = radioLabelsBaked !== void 0 || textLabelsBaked !== void 0 || checkboxLabelsBaked !== void 0;
1422
1470
  if (!hasEntries && !hasMarkers) return null;
1423
1471
  if (Object.keys(merged.fieldIdIndex).length === 0) delete merged.fieldIdIndex;
1424
1472
  return merged;
1425
1473
  }
1474
+ function mergeSourceMetadata(sources) {
1475
+ return assembleMetadata(sources);
1476
+ }
1426
1477
  function getSigniphiMetadata(pdfDoc) {
1427
- try {
1428
- const infoRef = pdfDoc.context.trailerInfo.Info;
1429
- if (!infoRef) {
1430
- return null;
1431
- }
1432
- const infoObj = pdfDoc.context.lookup(infoRef);
1433
- if (!infoObj || typeof infoObj.get !== "function") {
1434
- return null;
1435
- }
1436
- const infoDict = infoObj;
1437
- const metadataObj = infoDict.get(PDFName.of("SigniphiMetadata"));
1438
- if (!metadataObj) {
1439
- return null;
1440
- }
1441
- let jsonStr;
1442
- if (typeof metadataObj.decodeText === "function") {
1443
- jsonStr = metadataObj.decodeText();
1444
- } else {
1445
- const metadataStr = metadataObj.toString();
1446
- jsonStr = metadataStr;
1447
- if (metadataStr.startsWith("(") && metadataStr.endsWith(")")) {
1448
- jsonStr = metadataStr.slice(1, -1);
1449
- } else if (metadataStr.startsWith("<") && metadataStr.endsWith(">")) {
1450
- jsonStr = metadataStr.slice(1, -1);
1451
- }
1452
- }
1453
- const metadata = JSON.parse(jsonStr);
1454
- return metadata;
1455
- } catch (error) {
1456
- console.error("Failed to load Signiphi metadata:", error);
1457
- return null;
1478
+ const { metadata, blockPresent } = readMetadata(pdfDoc, { PDFName: PDFName });
1479
+ if (blockPresent && !metadata) {
1480
+ console.error("Failed to load Signiphi metadata: the block is present but will not parse");
1458
1481
  }
1482
+ return metadata;
1459
1483
  }
1460
1484
  function getBaseFieldName(encodedName) {
1461
1485
  let baseName = encodedName;
@@ -1514,6 +1538,10 @@ async function extractFieldsFromPdf(pdfBytes) {
1514
1538
  const metadata = getSigniphiMetadata(pdfDoc);
1515
1539
  const form = pdfDoc.getForm();
1516
1540
  const pdfFields = form.getFields();
1541
+ const pages = pdfDoc.getPages();
1542
+ const pageHeights = pages.map((p) => p.getHeight());
1543
+ const pageIndexByRef = new Map(pages.map((p, i) => [p.ref, i + 1]));
1544
+ const pageNumberOf = (ref) => ref && pageIndexByRef.get(ref) || 1;
1517
1545
  for (const field of pdfFields) {
1518
1546
  const fieldName = field.getName();
1519
1547
  const widgets = field.acroField.getWidgets();
@@ -1542,19 +1570,20 @@ async function extractFieldsFromPdf(pdfBytes) {
1542
1570
  } else {
1543
1571
  console.log(`[field-extraction] Unknown field type "${fieldName}" - defaulting required=false`);
1544
1572
  }
1573
+ const optionPositionsByPage = /* @__PURE__ */ new Map();
1574
+ if (widgetType === "radio") {
1575
+ for (const w of widgets) {
1576
+ const pageNumber = pageNumberOf(w.P());
1577
+ const r = w.getRectangle();
1578
+ const top = pageHeights[pageNumber - 1] - r.y - r.height;
1579
+ const bucket = optionPositionsByPage.get(pageNumber) ?? [];
1580
+ bucket.push({ x: r.x, y: top, width: r.width, height: r.height });
1581
+ optionPositionsByPage.set(pageNumber, bucket);
1582
+ }
1583
+ }
1545
1584
  for (const widget of widgets) {
1546
1585
  const rect = widget.getRectangle();
1547
- const pageRef = widget.P();
1548
- let pageNumber = 1;
1549
- if (pageRef) {
1550
- const pages = pdfDoc.getPages();
1551
- for (let i = 0; i < pages.length; i++) {
1552
- if (pages[i].ref === pageRef) {
1553
- pageNumber = i + 1;
1554
- break;
1555
- }
1556
- }
1557
- }
1586
+ const pageNumber = pageNumberOf(widget.P());
1558
1587
  const pageQualifiedKey = `${baseName}_p${pageNumber}`;
1559
1588
  let fieldMetadata = metadata?.fields?.[pageQualifiedKey];
1560
1589
  if (!fieldMetadata) {
@@ -1571,13 +1600,13 @@ async function extractFieldsFromPdf(pdfBytes) {
1571
1600
  if (fieldMetadata?.options?.length) {
1572
1601
  fieldOptions = fieldMetadata.options;
1573
1602
  }
1574
- const page = pdfDoc.getPage(pageNumber - 1);
1575
- const pageHeight = page.getHeight();
1603
+ const pageHeight = pageHeights[pageNumber - 1];
1576
1604
  const extractedField = {
1577
1605
  name: baseName,
1578
1606
  // Use clean base name without __SIGNER__ encoding (signer stored in signer property)
1579
- type: determineFieldType(fieldName, widgetType),
1580
- // Uses base name internally
1607
+ // Uses base name internally. A stored date_signed is a declaration the
1608
+ // name can't express, so it wins.
1609
+ type: fieldMetadata?.type === "date_signed" ? "date_signed" : determineFieldType(fieldName, widgetType),
1581
1610
  page: pageNumber,
1582
1611
  x: rect.x,
1583
1612
  y: pageHeight - rect.y - rect.height,
@@ -1589,6 +1618,9 @@ async function extractFieldsFromPdf(pdfBytes) {
1589
1618
  signer: signerFromName
1590
1619
  // Extract signer from encoded name
1591
1620
  };
1621
+ if (widgetType === "radio") {
1622
+ extractedField.optionPositions = optionPositionsByPage.get(pageNumber) ?? [];
1623
+ }
1592
1624
  if (fieldMetadata) {
1593
1625
  if (fieldMetadata.fieldId) {
1594
1626
  extractedField.fieldId = fieldMetadata.fieldId;
@@ -1634,20 +1666,14 @@ async function extractFieldsFromPdf(pdfBytes) {
1634
1666
  }
1635
1667
  function setSigniphiMetadata(pdfDoc, metadata) {
1636
1668
  try {
1637
- const infoRef = pdfDoc.context.trailerInfo.Info;
1638
- const infoObj = pdfDoc.context.lookup(infoRef);
1639
- if (!infoObj || typeof infoObj.set !== "function") {
1640
- throw new Error("Info object is not a PDFDict or does not have set method");
1641
- }
1642
- const infoDict = infoObj;
1643
- const metadataString = JSON.stringify(metadata);
1644
- infoDict.set(PDFName.of("SigniphiMetadata"), PDFHexString.fromText(metadataString));
1669
+ writeMetadata(pdfDoc, metadata, { PDFName: PDFName, PDFHexString: PDFHexString });
1645
1670
  } catch (error) {
1646
1671
  console.error("Failed to set Signiphi metadata:", error);
1647
1672
  throw error;
1648
1673
  }
1649
1674
  }
1650
1675
  var SIGNIPHI_CHECKBOX_LABEL_MARKER = "SiPL_Check";
1676
+ var RADIO_BORDER_WIDTH = 1;
1651
1677
  var FormFieldType = /* @__PURE__ */ ((FormFieldType2) => {
1652
1678
  FormFieldType2["TEXT"] = "text";
1653
1679
  FormFieldType2["SIGNATURE"] = "signature";
@@ -1657,6 +1683,7 @@ var FormFieldType = /* @__PURE__ */ ((FormFieldType2) => {
1657
1683
  FormFieldType2["RADIO"] = "radio";
1658
1684
  FormFieldType2["DROPDOWN"] = "dropdown";
1659
1685
  FormFieldType2["TEXT_LABEL"] = "text_label";
1686
+ FormFieldType2["DATE_SIGNED"] = "date_signed";
1660
1687
  return FormFieldType2;
1661
1688
  })(FormFieldType || {});
1662
1689
  function parseEncodedFieldName(encodedName) {
@@ -1887,7 +1914,10 @@ async function addFormFieldsToPdf(pdfBytes, formFields, options = {}) {
1887
1914
  }
1888
1915
  break;
1889
1916
  }
1890
- case "date" /* DATE */: {
1917
+ // DATE_SIGNED is the same widget; its metadata `type` (carried by the
1918
+ // merge, not by this pass) is what tells pdf-signer to stamp it.
1919
+ case "date" /* DATE */:
1920
+ case "date_signed" /* DATE_SIGNED */: {
1891
1921
  const baseWithSuffix = field.name.endsWith("_date") ? field.name : `${field.name}_date`;
1892
1922
  const finalName = conflictingNames.has(field.name) ? `${baseWithSuffix}_p${field.position.page}` : baseWithSuffix;
1893
1923
  const encodedName = encodeFieldNameWithLabelAndSigner(finalName, field.label, field.assignedSignerEmail);
@@ -1944,7 +1974,24 @@ async function addFormFieldsToPdf(pdfBytes, formFields, options = {}) {
1944
1974
  case "radio" /* RADIO */: {
1945
1975
  const encodedName = encodeFieldNameWithSigner(uniqueFieldName, field.assignedSignerEmail);
1946
1976
  const radioGroup = form.createRadioGroup(encodedName);
1947
- if (field.options && field.options.length > 0) {
1977
+ if (field.options && field.options.length > 0 && field.optionPositions?.length === field.options.length) {
1978
+ const optionPositions = field.optionPositions;
1979
+ field.options.forEach((option, index) => {
1980
+ const at = optionPositions[index];
1981
+ radioGroup.addOptionToPage(option, page, {
1982
+ x: at.x + RADIO_BORDER_WIDTH / 2,
1983
+ y: pageHeight - at.y - at.height + RADIO_BORDER_WIDTH / 2,
1984
+ width: at.width - RADIO_BORDER_WIDTH,
1985
+ height: at.height - RADIO_BORDER_WIDTH,
1986
+ borderWidth: RADIO_BORDER_WIDTH,
1987
+ borderColor: rgb(0.5, 0.5, 0.5),
1988
+ backgroundColor: rgb(1, 1, 1)
1989
+ });
1990
+ });
1991
+ if (field.defaultValue && field.options.includes(field.defaultValue)) {
1992
+ radioGroup.select(field.defaultValue);
1993
+ }
1994
+ } else if (field.options && field.options.length > 0) {
1948
1995
  const optionHeight = Math.min(20, field.position.height / field.options.length);
1949
1996
  const spacing = field.position.height / field.options.length;
1950
1997
  field.options.forEach((option, index) => {
@@ -3089,12 +3136,19 @@ var PageAssembler = forwardRef(
3089
3136
  if (sourcePdf && !sourceMetadata) {
3090
3137
  console.warn(`[PageAssembler] No readable SigniphiMetadata in "${fileName}"`);
3091
3138
  }
3139
+ const pageUsageForFile = filePageUsage.get(fileId) || /* @__PURE__ */ new Map();
3092
3140
  const placements = [];
3093
- metadataSources.push({ metadata: sourceMetadata, fields: placements });
3141
+ const pageLandings = [];
3142
+ for (const [srcIndex, pageInstances] of pageUsageForFile) {
3143
+ for (const pageInstance of pageInstances) {
3144
+ const combined = pageIdToNewPageNumber.get(pageInstance.id);
3145
+ if (combined !== void 0) pageLandings.push({ source: srcIndex + 1, combined });
3146
+ }
3147
+ }
3148
+ metadataSources.push({ metadata: sourceMetadata, fields: placements, pages: pageLandings });
3094
3149
  console.log(`[PageAssembler] Extracting fields from "${fileName}"`);
3095
3150
  const extractedFields = await extractFieldsFromPdf(pdfBytes2);
3096
3151
  console.log(`[PageAssembler] Found ${extractedFields.length} fields in "${fileName}"`);
3097
- const pageUsageForFile = filePageUsage.get(fileId) || /* @__PURE__ */ new Map();
3098
3152
  for (const field of extractedFields) {
3099
3153
  const originalPage = field.page;
3100
3154
  const srcIndex = originalPage - 1;
@@ -3118,6 +3172,7 @@ var PageAssembler = forwardRef(
3118
3172
  label: field.label,
3119
3173
  required: field.required,
3120
3174
  options: field.options,
3175
+ optionPositions: field.optionPositions,
3121
3176
  placeholder: field.placeholder,
3122
3177
  fieldId: field.fieldId,
3123
3178
  assignedSignerEmail: field.signer
@@ -3222,6 +3277,7 @@ var PageAssembler = forwardRef(
3222
3277
  label: field.label,
3223
3278
  required: field.required,
3224
3279
  options: field.options,
3280
+ optionPositions: field.optionPositions,
3225
3281
  placeholder: field.placeholder,
3226
3282
  assignedSignerEmail: field.assignedSignerEmail
3227
3283
  }));