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

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,8 +1600,7 @@ 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)
@@ -1589,6 +1617,9 @@ async function extractFieldsFromPdf(pdfBytes) {
1589
1617
  signer: signerFromName
1590
1618
  // Extract signer from encoded name
1591
1619
  };
1620
+ if (widgetType === "radio") {
1621
+ extractedField.optionPositions = optionPositionsByPage.get(pageNumber) ?? [];
1622
+ }
1592
1623
  if (fieldMetadata) {
1593
1624
  if (fieldMetadata.fieldId) {
1594
1625
  extractedField.fieldId = fieldMetadata.fieldId;
@@ -1634,20 +1665,14 @@ async function extractFieldsFromPdf(pdfBytes) {
1634
1665
  }
1635
1666
  function setSigniphiMetadata(pdfDoc, metadata) {
1636
1667
  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));
1668
+ writeMetadata(pdfDoc, metadata, { PDFName: PDFName, PDFHexString: PDFHexString });
1645
1669
  } catch (error) {
1646
1670
  console.error("Failed to set Signiphi metadata:", error);
1647
1671
  throw error;
1648
1672
  }
1649
1673
  }
1650
1674
  var SIGNIPHI_CHECKBOX_LABEL_MARKER = "SiPL_Check";
1675
+ var RADIO_BORDER_WIDTH = 1;
1651
1676
  var FormFieldType = /* @__PURE__ */ ((FormFieldType2) => {
1652
1677
  FormFieldType2["TEXT"] = "text";
1653
1678
  FormFieldType2["SIGNATURE"] = "signature";
@@ -1944,7 +1969,24 @@ async function addFormFieldsToPdf(pdfBytes, formFields, options = {}) {
1944
1969
  case "radio" /* RADIO */: {
1945
1970
  const encodedName = encodeFieldNameWithSigner(uniqueFieldName, field.assignedSignerEmail);
1946
1971
  const radioGroup = form.createRadioGroup(encodedName);
1947
- if (field.options && field.options.length > 0) {
1972
+ if (field.options && field.options.length > 0 && field.optionPositions?.length === field.options.length) {
1973
+ const optionPositions = field.optionPositions;
1974
+ field.options.forEach((option, index) => {
1975
+ const at = optionPositions[index];
1976
+ radioGroup.addOptionToPage(option, page, {
1977
+ x: at.x + RADIO_BORDER_WIDTH / 2,
1978
+ y: pageHeight - at.y - at.height + RADIO_BORDER_WIDTH / 2,
1979
+ width: at.width - RADIO_BORDER_WIDTH,
1980
+ height: at.height - RADIO_BORDER_WIDTH,
1981
+ borderWidth: RADIO_BORDER_WIDTH,
1982
+ borderColor: rgb(0.5, 0.5, 0.5),
1983
+ backgroundColor: rgb(1, 1, 1)
1984
+ });
1985
+ });
1986
+ if (field.defaultValue && field.options.includes(field.defaultValue)) {
1987
+ radioGroup.select(field.defaultValue);
1988
+ }
1989
+ } else if (field.options && field.options.length > 0) {
1948
1990
  const optionHeight = Math.min(20, field.position.height / field.options.length);
1949
1991
  const spacing = field.position.height / field.options.length;
1950
1992
  field.options.forEach((option, index) => {
@@ -3089,12 +3131,19 @@ var PageAssembler = forwardRef(
3089
3131
  if (sourcePdf && !sourceMetadata) {
3090
3132
  console.warn(`[PageAssembler] No readable SigniphiMetadata in "${fileName}"`);
3091
3133
  }
3134
+ const pageUsageForFile = filePageUsage.get(fileId) || /* @__PURE__ */ new Map();
3092
3135
  const placements = [];
3093
- metadataSources.push({ metadata: sourceMetadata, fields: placements });
3136
+ const pageLandings = [];
3137
+ for (const [srcIndex, pageInstances] of pageUsageForFile) {
3138
+ for (const pageInstance of pageInstances) {
3139
+ const combined = pageIdToNewPageNumber.get(pageInstance.id);
3140
+ if (combined !== void 0) pageLandings.push({ source: srcIndex + 1, combined });
3141
+ }
3142
+ }
3143
+ metadataSources.push({ metadata: sourceMetadata, fields: placements, pages: pageLandings });
3094
3144
  console.log(`[PageAssembler] Extracting fields from "${fileName}"`);
3095
3145
  const extractedFields = await extractFieldsFromPdf(pdfBytes2);
3096
3146
  console.log(`[PageAssembler] Found ${extractedFields.length} fields in "${fileName}"`);
3097
- const pageUsageForFile = filePageUsage.get(fileId) || /* @__PURE__ */ new Map();
3098
3147
  for (const field of extractedFields) {
3099
3148
  const originalPage = field.page;
3100
3149
  const srcIndex = originalPage - 1;
@@ -3118,6 +3167,7 @@ var PageAssembler = forwardRef(
3118
3167
  label: field.label,
3119
3168
  required: field.required,
3120
3169
  options: field.options,
3170
+ optionPositions: field.optionPositions,
3121
3171
  placeholder: field.placeholder,
3122
3172
  fieldId: field.fieldId,
3123
3173
  assignedSignerEmail: field.signer
@@ -3222,6 +3272,7 @@ var PageAssembler = forwardRef(
3222
3272
  label: field.label,
3223
3273
  required: field.required,
3224
3274
  options: field.options,
3275
+ optionPositions: field.optionPositions,
3225
3276
  placeholder: field.placeholder,
3226
3277
  assignedSignerEmail: field.assignedSignerEmail
3227
3278
  }));