@signiphi/page-assembly 0.2.0-beta.13 → 0.2.0-beta.15

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.d.mts CHANGED
@@ -309,24 +309,93 @@ interface ExtractedField {
309
309
  signer?: string;
310
310
  }
311
311
  /**
312
- * SigniphiMetadata structure stored in PDF Info dictionary
312
+ * One field's metadata entry, as stored in the block.
313
+ */
314
+ interface SigniphiFieldMetadata {
315
+ fieldId?: string;
316
+ label?: string;
317
+ signer?: string;
318
+ placeholder?: string;
319
+ required?: boolean;
320
+ options?: string[];
321
+ acknowledgements?: Array<{
322
+ id: string;
323
+ title: string;
324
+ description: string;
325
+ }>;
326
+ /**
327
+ * TEXT_LABEL only: this entry's text is already in the page stream, so the
328
+ * sign-time pass must not redraw it. See `mergeSourceMetadata`, which sets it
329
+ * when a source that had baked its labels is merged with one that deferred.
330
+ */
331
+ textLabelBaked?: boolean;
332
+ /**
333
+ * CHECKBOX only: this field's caption is already in the page stream, written
334
+ * by document-prepare's bake. Declared so the merge carries it through — it
335
+ * is what pdf-signer's `checkboxLabelSource` actually decides on, the
336
+ * document-level `checkboxLabelsBaked` below being only the bake's mode.
337
+ * Nothing here sets or reads it.
338
+ */
339
+ checkboxCaptionBaked?: boolean;
340
+ /**
341
+ * Persisted geometry, written by `document-prepare` for RADIO and TEXT_LABEL
342
+ * fields only. Declared here for ONE reason: its `page` is the SOURCE
343
+ * document's page number, and the merge has to rewrite it to the combined
344
+ * one. Nothing in page-assembly reads x/y/width/height.
345
+ */
346
+ position?: SigniphiFieldPosition;
347
+ /** Radio-only, per-option. Re-paged alongside {@link SigniphiFieldMetadata.position}. */
348
+ radioPositions?: SigniphiFieldPosition[];
349
+ /** Radio-only bound list (options ↔ circle positions); its pages are re-paged too. */
350
+ radioOptions?: Array<{
351
+ label: string;
352
+ exportValue: string;
353
+ position: SigniphiFieldPosition;
354
+ }>;
355
+ }
356
+ /**
357
+ * A persisted field rect, as `document-prepare` writes it. `page` is 1-indexed
358
+ * and scoped to the document it was written in — which is exactly why the merge
359
+ * cannot carry it across unchanged.
360
+ */
361
+ interface SigniphiFieldPosition {
362
+ x: number;
363
+ y: number;
364
+ width: number;
365
+ height: number;
366
+ page: number;
367
+ }
368
+ /**
369
+ * SigniphiMetadata structure stored in PDF Info dictionary.
370
+ *
371
+ * Exported because `getSigniphiMetadata` / `setSigniphiMetadata` are public
372
+ * (`utils/index.ts`) and a caller cannot name their argument otherwise — which
373
+ * is what pushed the call sites here into `as never` casts that would have
374
+ * silenced any future shape mismatch.
313
375
  */
314
376
  interface SigniphiMetadata {
315
377
  version: string;
316
- fields: Record<string, {
317
- fieldId?: string;
318
- label?: string;
319
- signer?: string;
320
- placeholder?: string;
321
- required?: boolean;
322
- options?: string[];
323
- acknowledgements?: Array<{
324
- id: string;
325
- title: string;
326
- description: string;
327
- }>;
328
- }>;
378
+ fields: Record<string, SigniphiFieldMetadata>;
329
379
  fieldIdIndex?: Record<string, string>;
380
+ /**
381
+ * Document-level: radio group and option text is already drawn into the page
382
+ * stream, so the sign-time pass must not redraw it. Absent on legacy PDFs,
383
+ * which pdf-signer reads as "draw".
384
+ */
385
+ radioLabelsBaked?: boolean;
386
+ /**
387
+ * Document-level: `false` means TEXT_LABEL text was deliberately NOT baked
388
+ * (multi-signer), so the sign-time pass redraws it per signer. Absent/`true`
389
+ * means it is already in the page stream.
390
+ */
391
+ textLabelsBaked?: boolean;
392
+ /**
393
+ * Document-level: `false` means CHECKBOX captions were deliberately NOT baked
394
+ * beside their boxes (multi-signer), so the sign-time pass draws them per
395
+ * signer. `true` means they are already in the page stream. Absent means the
396
+ * source predates baked captions, which pdf-signer also reads as "draw".
397
+ */
398
+ checkboxLabelsBaked?: boolean;
330
399
  }
331
400
  /**
332
401
  * Extract SigniphiMetadata from a PDF document
package/dist/index.d.ts CHANGED
@@ -309,24 +309,93 @@ interface ExtractedField {
309
309
  signer?: string;
310
310
  }
311
311
  /**
312
- * SigniphiMetadata structure stored in PDF Info dictionary
312
+ * One field's metadata entry, as stored in the block.
313
+ */
314
+ interface SigniphiFieldMetadata {
315
+ fieldId?: string;
316
+ label?: string;
317
+ signer?: string;
318
+ placeholder?: string;
319
+ required?: boolean;
320
+ options?: string[];
321
+ acknowledgements?: Array<{
322
+ id: string;
323
+ title: string;
324
+ description: string;
325
+ }>;
326
+ /**
327
+ * TEXT_LABEL only: this entry's text is already in the page stream, so the
328
+ * sign-time pass must not redraw it. See `mergeSourceMetadata`, which sets it
329
+ * when a source that had baked its labels is merged with one that deferred.
330
+ */
331
+ textLabelBaked?: boolean;
332
+ /**
333
+ * CHECKBOX only: this field's caption is already in the page stream, written
334
+ * by document-prepare's bake. Declared so the merge carries it through — it
335
+ * is what pdf-signer's `checkboxLabelSource` actually decides on, the
336
+ * document-level `checkboxLabelsBaked` below being only the bake's mode.
337
+ * Nothing here sets or reads it.
338
+ */
339
+ checkboxCaptionBaked?: boolean;
340
+ /**
341
+ * Persisted geometry, written by `document-prepare` for RADIO and TEXT_LABEL
342
+ * fields only. Declared here for ONE reason: its `page` is the SOURCE
343
+ * document's page number, and the merge has to rewrite it to the combined
344
+ * one. Nothing in page-assembly reads x/y/width/height.
345
+ */
346
+ position?: SigniphiFieldPosition;
347
+ /** Radio-only, per-option. Re-paged alongside {@link SigniphiFieldMetadata.position}. */
348
+ radioPositions?: SigniphiFieldPosition[];
349
+ /** Radio-only bound list (options ↔ circle positions); its pages are re-paged too. */
350
+ radioOptions?: Array<{
351
+ label: string;
352
+ exportValue: string;
353
+ position: SigniphiFieldPosition;
354
+ }>;
355
+ }
356
+ /**
357
+ * A persisted field rect, as `document-prepare` writes it. `page` is 1-indexed
358
+ * and scoped to the document it was written in — which is exactly why the merge
359
+ * cannot carry it across unchanged.
360
+ */
361
+ interface SigniphiFieldPosition {
362
+ x: number;
363
+ y: number;
364
+ width: number;
365
+ height: number;
366
+ page: number;
367
+ }
368
+ /**
369
+ * SigniphiMetadata structure stored in PDF Info dictionary.
370
+ *
371
+ * Exported because `getSigniphiMetadata` / `setSigniphiMetadata` are public
372
+ * (`utils/index.ts`) and a caller cannot name their argument otherwise — which
373
+ * is what pushed the call sites here into `as never` casts that would have
374
+ * silenced any future shape mismatch.
313
375
  */
314
376
  interface SigniphiMetadata {
315
377
  version: string;
316
- fields: Record<string, {
317
- fieldId?: string;
318
- label?: string;
319
- signer?: string;
320
- placeholder?: string;
321
- required?: boolean;
322
- options?: string[];
323
- acknowledgements?: Array<{
324
- id: string;
325
- title: string;
326
- description: string;
327
- }>;
328
- }>;
378
+ fields: Record<string, SigniphiFieldMetadata>;
329
379
  fieldIdIndex?: Record<string, string>;
380
+ /**
381
+ * Document-level: radio group and option text is already drawn into the page
382
+ * stream, so the sign-time pass must not redraw it. Absent on legacy PDFs,
383
+ * which pdf-signer reads as "draw".
384
+ */
385
+ radioLabelsBaked?: boolean;
386
+ /**
387
+ * Document-level: `false` means TEXT_LABEL text was deliberately NOT baked
388
+ * (multi-signer), so the sign-time pass redraws it per signer. Absent/`true`
389
+ * means it is already in the page stream.
390
+ */
391
+ textLabelsBaked?: boolean;
392
+ /**
393
+ * Document-level: `false` means CHECKBOX captions were deliberately NOT baked
394
+ * beside their boxes (multi-signer), so the sign-time pass draws them per
395
+ * signer. `true` means they are already in the page stream. Absent means the
396
+ * source predates baked captions, which pdf-signer also reads as "draw".
397
+ */
398
+ checkboxLabelsBaked?: boolean;
330
399
  }
331
400
  /**
332
401
  * Extract SigniphiMetadata from a PDF document
package/dist/index.js CHANGED
@@ -1399,6 +1399,55 @@ function downloadPdf(pdfBytes, filename) {
1399
1399
  a.click();
1400
1400
  URL.revokeObjectURL(url);
1401
1401
  }
1402
+ function toCombinedPage(entry, page) {
1403
+ if (!entry.position && !entry.radioPositions && !entry.radioOptions) return entry;
1404
+ const carried = { ...entry };
1405
+ if (entry.position) carried.position = { ...entry.position, page };
1406
+ if (entry.radioPositions) carried.radioPositions = entry.radioPositions.map((p) => ({ ...p, page }));
1407
+ if (entry.radioOptions) {
1408
+ carried.radioOptions = entry.radioOptions.map((o) => ({ ...o, position: { ...o.position, page } }));
1409
+ }
1410
+ return carried;
1411
+ }
1412
+ function mergeSourceMetadata(sources) {
1413
+ const merged = { version: "1.0", fields: {}, fieldIdIndex: {} };
1414
+ let radioLabelsBaked;
1415
+ let textLabelsBaked;
1416
+ let checkboxLabelsBaked;
1417
+ for (const source of sources) {
1418
+ const meta = source.metadata;
1419
+ if (!meta || source.fields.length === 0) continue;
1420
+ if (meta.radioLabelsBaked === false) radioLabelsBaked = false;
1421
+ else if (meta.radioLabelsBaked === true && radioLabelsBaked === void 0) radioLabelsBaked = true;
1422
+ if (meta.textLabelsBaked === false) textLabelsBaked = false;
1423
+ else if (meta.textLabelsBaked === true && textLabelsBaked === void 0) textLabelsBaked = true;
1424
+ if (meta.checkboxLabelsBaked === false) checkboxLabelsBaked = false;
1425
+ else if (meta.checkboxLabelsBaked === true && checkboxLabelsBaked === void 0) checkboxLabelsBaked = true;
1426
+ }
1427
+ for (const source of sources) {
1428
+ const meta = source.metadata;
1429
+ if (!meta) continue;
1430
+ const sourceTextLabelsBaked = meta.textLabelsBaked !== false;
1431
+ for (const { name, page } of source.fields) {
1432
+ const key = `${name}_p${page}`;
1433
+ const entry = meta.fields?.[name];
1434
+ if (entry) {
1435
+ const carried = toCombinedPage(entry, page);
1436
+ merged.fields[key] = textLabelsBaked === false && sourceTextLabelsBaked ? { ...carried, textLabelBaked: true } : carried;
1437
+ }
1438
+ const fieldId = meta.fieldIdIndex?.[name];
1439
+ if (fieldId) merged.fieldIdIndex[key] = fieldId;
1440
+ }
1441
+ }
1442
+ if (radioLabelsBaked !== void 0) merged.radioLabelsBaked = radioLabelsBaked;
1443
+ if (textLabelsBaked !== void 0) merged.textLabelsBaked = textLabelsBaked;
1444
+ if (checkboxLabelsBaked !== void 0) merged.checkboxLabelsBaked = checkboxLabelsBaked;
1445
+ const hasEntries = Object.keys(merged.fields).length > 0;
1446
+ const hasMarkers = radioLabelsBaked !== void 0 || textLabelsBaked !== void 0 || checkboxLabelsBaked !== void 0;
1447
+ if (!hasEntries && !hasMarkers) return null;
1448
+ if (Object.keys(merged.fieldIdIndex).length === 0) delete merged.fieldIdIndex;
1449
+ return merged;
1450
+ }
1402
1451
  function getSigniphiMetadata(pdfDoc) {
1403
1452
  try {
1404
1453
  const infoRef = pdfDoc.context.trailerInfo.Info;
@@ -1414,12 +1463,17 @@ function getSigniphiMetadata(pdfDoc) {
1414
1463
  if (!metadataObj) {
1415
1464
  return null;
1416
1465
  }
1417
- const metadataStr = metadataObj.toString();
1418
- let jsonStr = metadataStr;
1419
- if (metadataStr.startsWith("(") && metadataStr.endsWith(")")) {
1420
- jsonStr = metadataStr.slice(1, -1);
1421
- } else if (metadataStr.startsWith("<") && metadataStr.endsWith(">")) {
1422
- jsonStr = metadataStr.slice(1, -1);
1466
+ let jsonStr;
1467
+ if (typeof metadataObj.decodeText === "function") {
1468
+ jsonStr = metadataObj.decodeText();
1469
+ } else {
1470
+ const metadataStr = metadataObj.toString();
1471
+ jsonStr = metadataStr;
1472
+ if (metadataStr.startsWith("(") && metadataStr.endsWith(")")) {
1473
+ jsonStr = metadataStr.slice(1, -1);
1474
+ } else if (metadataStr.startsWith("<") && metadataStr.endsWith(">")) {
1475
+ jsonStr = metadataStr.slice(1, -1);
1476
+ }
1423
1477
  }
1424
1478
  const metadata = JSON.parse(jsonStr);
1425
1479
  return metadata;
@@ -1612,12 +1666,13 @@ function setSigniphiMetadata(pdfDoc, metadata) {
1612
1666
  }
1613
1667
  const infoDict = infoObj;
1614
1668
  const metadataString = JSON.stringify(metadata);
1615
- infoDict.set(pdfLib.PDFName.of("SigniphiMetadata"), pdfLib.PDFString.of(metadataString));
1669
+ infoDict.set(pdfLib.PDFName.of("SigniphiMetadata"), pdfLib.PDFHexString.fromText(metadataString));
1616
1670
  } catch (error) {
1617
1671
  console.error("Failed to set Signiphi metadata:", error);
1618
1672
  throw error;
1619
1673
  }
1620
1674
  }
1675
+ var SIGNIPHI_CHECKBOX_LABEL_MARKER = "SiPL_Check";
1621
1676
  var FormFieldType = /* @__PURE__ */ ((FormFieldType2) => {
1622
1677
  FormFieldType2["TEXT"] = "text";
1623
1678
  FormFieldType2["SIGNATURE"] = "signature";
@@ -1703,6 +1758,7 @@ async function addFormFieldsToPdf(pdfBytes, formFields, options = {}) {
1703
1758
  }
1704
1759
  }
1705
1760
  const font = await pdfDoc.embedFont(pdfLib.StandardFonts.Helvetica);
1761
+ const bakedCheckboxCaptions = /* @__PURE__ */ new Map();
1706
1762
  const fieldNamePages = /* @__PURE__ */ new Map();
1707
1763
  for (const field of formFields) {
1708
1764
  const baseName = field.name;
@@ -1799,10 +1855,15 @@ async function addFormFieldsToPdf(pdfBytes, formFields, options = {}) {
1799
1855
  } catch {
1800
1856
  }
1801
1857
  if (drawLabels) {
1802
- const fontSize = Math.min(12, field.position.height * 0.6);
1858
+ const fontSize = Math.max(1, Math.min(10, field.position.height * 0.4));
1803
1859
  const labelX = pdfX + field.position.width + 5;
1804
1860
  const labelY = pdfY + (field.position.height - fontSize) / 2;
1861
+ page.pushOperators(
1862
+ pdfLib.PDFOperator.of(pdfLib.PDFOperatorNames.BeginMarkedContent, [pdfLib.PDFName.of(SIGNIPHI_CHECKBOX_LABEL_MARKER)])
1863
+ );
1805
1864
  page.drawText(field.label, { x: labelX, y: labelY, size: fontSize, font, color: pdfLib.rgb(0, 0, 0) });
1865
+ page.pushOperators(pdfLib.PDFOperator.of(pdfLib.PDFOperatorNames.EndMarkedContent));
1866
+ bakedCheckboxCaptions.set(encodedName, field);
1806
1867
  }
1807
1868
  }
1808
1869
  break;
@@ -1954,8 +2015,28 @@ async function addFormFieldsToPdf(pdfBytes, formFields, options = {}) {
1954
2015
  console.error(`Error adding field ${field.name}:`, error);
1955
2016
  }
1956
2017
  }
2018
+ recordBakedCheckboxCaptions(pdfDoc, bakedCheckboxCaptions);
1957
2019
  return await pdfDoc.save();
1958
2020
  }
2021
+ function recordBakedCheckboxCaptions(pdfDoc, captions) {
2022
+ if (captions.size === 0) return;
2023
+ try {
2024
+ const metadata = getSigniphiMetadata(pdfDoc);
2025
+ if (!metadata) return;
2026
+ metadata.fields ?? (metadata.fields = {});
2027
+ for (const [name, field] of captions) {
2028
+ metadata.fields[name] = {
2029
+ signer: field.assignedSignerEmail,
2030
+ label: field.label,
2031
+ ...metadata.fields[name],
2032
+ checkboxCaptionBaked: true
2033
+ };
2034
+ }
2035
+ setSigniphiMetadata(pdfDoc, metadata);
2036
+ } catch (error) {
2037
+ console.warn("Failed to record baked checkbox captions:", error);
2038
+ }
2039
+ }
1959
2040
  function mapFieldPositionsAfterAssembly(fields, pageMapping) {
1960
2041
  return fields.map((field) => {
1961
2042
  const mappingKey = String(field.position.page - 1);
@@ -2925,7 +3006,7 @@ var PageAssembler = React7.forwardRef(
2925
3006
  setIsLoading(true);
2926
3007
  setProcessingStatus("Assembling PDF...");
2927
3008
  try {
2928
- const { PDFDocument: PDFDocument5, PDFName: PDFName4, PDFString: PDFString4 } = await import('pdf-lib');
3009
+ const { PDFDocument: PDFDocument5, PDFName: PDFName4 } = await import('pdf-lib');
2929
3010
  const loadedPdfs = /* @__PURE__ */ new Map();
2930
3011
  const fileMap = /* @__PURE__ */ new Map();
2931
3012
  const pdfFiles = state.files.filter((f) => f.type === "pdf" && f.originalBytes?.length > 0);
@@ -3004,11 +3085,7 @@ var PageAssembler = React7.forwardRef(
3004
3085
  }
3005
3086
  console.log("[PageAssembler] Starting multi-PDF assembly with field preservation");
3006
3087
  const allExtractedFields = [];
3007
- const mergedMetadata = {
3008
- version: "1.0",
3009
- fields: {},
3010
- fieldIdIndex: {}
3011
- };
3088
+ const metadataSources = [];
3012
3089
  const filePageUsage = /* @__PURE__ */ new Map();
3013
3090
  for (const file of state.files) {
3014
3091
  for (const page of file.pages) {
@@ -3033,28 +3110,12 @@ var PageAssembler = React7.forwardRef(
3033
3110
  }
3034
3111
  console.log("[PageAssembler] Page mapping:", Object.fromEntries(pageIdToNewPageNumber));
3035
3112
  const extractAndMapFields = async (fileId, fileName, pdfBytes2, sourcePdf) => {
3036
- let sourceMetadata = null;
3037
- if (sourcePdf) {
3038
- try {
3039
- const sourceInfoRef = sourcePdf.context.trailerInfo.Info;
3040
- if (sourceInfoRef) {
3041
- const sourceInfoDict = sourcePdf.context.lookup(sourceInfoRef);
3042
- if (sourceInfoDict && typeof sourceInfoDict.get === "function") {
3043
- const metadataObj = sourceInfoDict.get(PDFName4.of("SigniphiMetadata"));
3044
- if (metadataObj) {
3045
- const metadataStr = metadataObj.toString();
3046
- let jsonStr = metadataStr;
3047
- if (metadataStr.startsWith("(") && metadataStr.endsWith(")")) {
3048
- jsonStr = metadataStr.slice(1, -1);
3049
- }
3050
- sourceMetadata = JSON.parse(jsonStr);
3051
- }
3052
- }
3053
- }
3054
- } catch (metadataError) {
3055
- console.warn(`Could not read metadata from PDF ${fileName}:`, metadataError);
3056
- }
3113
+ const sourceMetadata = sourcePdf ? getSigniphiMetadata(sourcePdf) : null;
3114
+ if (sourcePdf && !sourceMetadata) {
3115
+ console.warn(`[PageAssembler] No readable SigniphiMetadata in "${fileName}"`);
3057
3116
  }
3117
+ const placements = [];
3118
+ metadataSources.push({ metadata: sourceMetadata, fields: placements });
3058
3119
  console.log(`[PageAssembler] Extracting fields from "${fileName}"`);
3059
3120
  const extractedFields = await extractFieldsFromPdf(pdfBytes2);
3060
3121
  console.log(`[PageAssembler] Found ${extractedFields.length} fields in "${fileName}"`);
@@ -3088,13 +3149,7 @@ var PageAssembler = React7.forwardRef(
3088
3149
  };
3089
3150
  allExtractedFields.push(trackedField);
3090
3151
  console.log(`[PageAssembler] Mapped field "${field.name}" from page ${originalPage} to page ${newPageNumber}`);
3091
- const metadataKey = `${field.name}_p${newPageNumber}`;
3092
- if (sourceMetadata?.fields?.[field.name]) {
3093
- mergedMetadata.fields[metadataKey] = sourceMetadata.fields[field.name];
3094
- }
3095
- if (sourceMetadata?.fieldIdIndex?.[field.name]) {
3096
- mergedMetadata.fieldIdIndex[metadataKey] = sourceMetadata.fieldIdIndex[field.name];
3097
- }
3152
+ placements.push({ name: field.name, page: newPageNumber });
3098
3153
  }
3099
3154
  }
3100
3155
  }
@@ -3172,21 +3227,15 @@ var PageAssembler = React7.forwardRef(
3172
3227
  }
3173
3228
  }
3174
3229
  }
3175
- if (Object.keys(mergedMetadata.fields).length > 0) {
3230
+ const mergedMetadata = mergeSourceMetadata(metadataSources);
3231
+ if (mergedMetadata) {
3176
3232
  try {
3177
- const newInfoRef = combinedDoc.context.trailerInfo.Info;
3178
- if (newInfoRef) {
3179
- const newInfoDict = combinedDoc.context.lookup(newInfoRef);
3180
- if (newInfoDict && typeof newInfoDict.set === "function") {
3181
- newInfoDict.set(
3182
- PDFName4.of("SigniphiMetadata"),
3183
- PDFString4.of(JSON.stringify(mergedMetadata))
3184
- );
3185
- }
3186
- }
3233
+ setSigniphiMetadata(combinedDoc, mergedMetadata);
3187
3234
  } catch (metadataError) {
3188
3235
  console.warn("Could not write merged metadata:", metadataError);
3189
3236
  }
3237
+ } else {
3238
+ console.warn("[PageAssembler] No source metadata survived the merge; the assembled document will read as unprepared downstream");
3190
3239
  }
3191
3240
  let pdfBytes = await combinedDoc.save();
3192
3241
  if (allExtractedFields.length > 0) {