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

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,78 @@ 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
+ * Persisted geometry, written by `document-prepare` for RADIO and TEXT_LABEL
334
+ * fields only. Declared here for ONE reason: its `page` is the SOURCE
335
+ * document's page number, and the merge has to rewrite it to the combined
336
+ * one. Nothing in page-assembly reads x/y/width/height.
337
+ */
338
+ position?: SigniphiFieldPosition;
339
+ /** Radio-only, per-option. Re-paged alongside {@link SigniphiFieldMetadata.position}. */
340
+ radioPositions?: SigniphiFieldPosition[];
341
+ /** Radio-only bound list (options ↔ circle positions); its pages are re-paged too. */
342
+ radioOptions?: Array<{
343
+ label: string;
344
+ exportValue: string;
345
+ position: SigniphiFieldPosition;
346
+ }>;
347
+ }
348
+ /**
349
+ * A persisted field rect, as `document-prepare` writes it. `page` is 1-indexed
350
+ * and scoped to the document it was written in — which is exactly why the merge
351
+ * cannot carry it across unchanged.
352
+ */
353
+ interface SigniphiFieldPosition {
354
+ x: number;
355
+ y: number;
356
+ width: number;
357
+ height: number;
358
+ page: number;
359
+ }
360
+ /**
361
+ * SigniphiMetadata structure stored in PDF Info dictionary.
362
+ *
363
+ * Exported because `getSigniphiMetadata` / `setSigniphiMetadata` are public
364
+ * (`utils/index.ts`) and a caller cannot name their argument otherwise — which
365
+ * is what pushed the call sites here into `as never` casts that would have
366
+ * silenced any future shape mismatch.
313
367
  */
314
368
  interface SigniphiMetadata {
315
369
  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
- }>;
370
+ fields: Record<string, SigniphiFieldMetadata>;
329
371
  fieldIdIndex?: Record<string, string>;
372
+ /**
373
+ * Document-level: radio group and option text is already drawn into the page
374
+ * stream, so the sign-time pass must not redraw it. Absent on legacy PDFs,
375
+ * which pdf-signer reads as "draw".
376
+ */
377
+ radioLabelsBaked?: boolean;
378
+ /**
379
+ * Document-level: `false` means TEXT_LABEL text was deliberately NOT baked
380
+ * (multi-signer), so the sign-time pass redraws it per signer. Absent/`true`
381
+ * means it is already in the page stream.
382
+ */
383
+ textLabelsBaked?: boolean;
330
384
  }
331
385
  /**
332
386
  * Extract SigniphiMetadata from a PDF document
package/dist/index.d.ts CHANGED
@@ -309,24 +309,78 @@ 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
+ * Persisted geometry, written by `document-prepare` for RADIO and TEXT_LABEL
334
+ * fields only. Declared here for ONE reason: its `page` is the SOURCE
335
+ * document's page number, and the merge has to rewrite it to the combined
336
+ * one. Nothing in page-assembly reads x/y/width/height.
337
+ */
338
+ position?: SigniphiFieldPosition;
339
+ /** Radio-only, per-option. Re-paged alongside {@link SigniphiFieldMetadata.position}. */
340
+ radioPositions?: SigniphiFieldPosition[];
341
+ /** Radio-only bound list (options ↔ circle positions); its pages are re-paged too. */
342
+ radioOptions?: Array<{
343
+ label: string;
344
+ exportValue: string;
345
+ position: SigniphiFieldPosition;
346
+ }>;
347
+ }
348
+ /**
349
+ * A persisted field rect, as `document-prepare` writes it. `page` is 1-indexed
350
+ * and scoped to the document it was written in — which is exactly why the merge
351
+ * cannot carry it across unchanged.
352
+ */
353
+ interface SigniphiFieldPosition {
354
+ x: number;
355
+ y: number;
356
+ width: number;
357
+ height: number;
358
+ page: number;
359
+ }
360
+ /**
361
+ * SigniphiMetadata structure stored in PDF Info dictionary.
362
+ *
363
+ * Exported because `getSigniphiMetadata` / `setSigniphiMetadata` are public
364
+ * (`utils/index.ts`) and a caller cannot name their argument otherwise — which
365
+ * is what pushed the call sites here into `as never` casts that would have
366
+ * silenced any future shape mismatch.
313
367
  */
314
368
  interface SigniphiMetadata {
315
369
  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
- }>;
370
+ fields: Record<string, SigniphiFieldMetadata>;
329
371
  fieldIdIndex?: Record<string, string>;
372
+ /**
373
+ * Document-level: radio group and option text is already drawn into the page
374
+ * stream, so the sign-time pass must not redraw it. Absent on legacy PDFs,
375
+ * which pdf-signer reads as "draw".
376
+ */
377
+ radioLabelsBaked?: boolean;
378
+ /**
379
+ * Document-level: `false` means TEXT_LABEL text was deliberately NOT baked
380
+ * (multi-signer), so the sign-time pass redraws it per signer. Absent/`true`
381
+ * means it is already in the page stream.
382
+ */
383
+ textLabelsBaked?: boolean;
330
384
  }
331
385
  /**
332
386
  * Extract SigniphiMetadata from a PDF document
package/dist/index.js CHANGED
@@ -1399,6 +1399,51 @@ 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
+ for (const source of sources) {
1417
+ const meta = source.metadata;
1418
+ if (!meta || source.fields.length === 0) continue;
1419
+ if (meta.radioLabelsBaked === false) radioLabelsBaked = false;
1420
+ else if (meta.radioLabelsBaked === true && radioLabelsBaked === void 0) radioLabelsBaked = true;
1421
+ if (meta.textLabelsBaked === false) textLabelsBaked = false;
1422
+ else if (meta.textLabelsBaked === true && textLabelsBaked === void 0) textLabelsBaked = true;
1423
+ }
1424
+ for (const source of sources) {
1425
+ const meta = source.metadata;
1426
+ if (!meta) continue;
1427
+ const sourceTextLabelsBaked = meta.textLabelsBaked !== false;
1428
+ for (const { name, page } of source.fields) {
1429
+ const key = `${name}_p${page}`;
1430
+ const entry = meta.fields?.[name];
1431
+ if (entry) {
1432
+ const carried = toCombinedPage(entry, page);
1433
+ merged.fields[key] = textLabelsBaked === false && sourceTextLabelsBaked ? { ...carried, textLabelBaked: true } : carried;
1434
+ }
1435
+ const fieldId = meta.fieldIdIndex?.[name];
1436
+ if (fieldId) merged.fieldIdIndex[key] = fieldId;
1437
+ }
1438
+ }
1439
+ if (radioLabelsBaked !== void 0) merged.radioLabelsBaked = radioLabelsBaked;
1440
+ if (textLabelsBaked !== void 0) merged.textLabelsBaked = textLabelsBaked;
1441
+ const hasEntries = Object.keys(merged.fields).length > 0;
1442
+ const hasMarkers = radioLabelsBaked !== void 0 || textLabelsBaked !== void 0;
1443
+ if (!hasEntries && !hasMarkers) return null;
1444
+ if (Object.keys(merged.fieldIdIndex).length === 0) delete merged.fieldIdIndex;
1445
+ return merged;
1446
+ }
1402
1447
  function getSigniphiMetadata(pdfDoc) {
1403
1448
  try {
1404
1449
  const infoRef = pdfDoc.context.trailerInfo.Info;
@@ -1414,12 +1459,17 @@ function getSigniphiMetadata(pdfDoc) {
1414
1459
  if (!metadataObj) {
1415
1460
  return null;
1416
1461
  }
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);
1462
+ let jsonStr;
1463
+ if (typeof metadataObj.decodeText === "function") {
1464
+ jsonStr = metadataObj.decodeText();
1465
+ } else {
1466
+ const metadataStr = metadataObj.toString();
1467
+ jsonStr = metadataStr;
1468
+ if (metadataStr.startsWith("(") && metadataStr.endsWith(")")) {
1469
+ jsonStr = metadataStr.slice(1, -1);
1470
+ } else if (metadataStr.startsWith("<") && metadataStr.endsWith(">")) {
1471
+ jsonStr = metadataStr.slice(1, -1);
1472
+ }
1423
1473
  }
1424
1474
  const metadata = JSON.parse(jsonStr);
1425
1475
  return metadata;
@@ -1612,7 +1662,7 @@ function setSigniphiMetadata(pdfDoc, metadata) {
1612
1662
  }
1613
1663
  const infoDict = infoObj;
1614
1664
  const metadataString = JSON.stringify(metadata);
1615
- infoDict.set(pdfLib.PDFName.of("SigniphiMetadata"), pdfLib.PDFString.of(metadataString));
1665
+ infoDict.set(pdfLib.PDFName.of("SigniphiMetadata"), pdfLib.PDFHexString.fromText(metadataString));
1616
1666
  } catch (error) {
1617
1667
  console.error("Failed to set Signiphi metadata:", error);
1618
1668
  throw error;
@@ -2925,7 +2975,7 @@ var PageAssembler = React7.forwardRef(
2925
2975
  setIsLoading(true);
2926
2976
  setProcessingStatus("Assembling PDF...");
2927
2977
  try {
2928
- const { PDFDocument: PDFDocument5, PDFName: PDFName4, PDFString: PDFString4 } = await import('pdf-lib');
2978
+ const { PDFDocument: PDFDocument5, PDFName: PDFName4 } = await import('pdf-lib');
2929
2979
  const loadedPdfs = /* @__PURE__ */ new Map();
2930
2980
  const fileMap = /* @__PURE__ */ new Map();
2931
2981
  const pdfFiles = state.files.filter((f) => f.type === "pdf" && f.originalBytes?.length > 0);
@@ -3004,11 +3054,7 @@ var PageAssembler = React7.forwardRef(
3004
3054
  }
3005
3055
  console.log("[PageAssembler] Starting multi-PDF assembly with field preservation");
3006
3056
  const allExtractedFields = [];
3007
- const mergedMetadata = {
3008
- version: "1.0",
3009
- fields: {},
3010
- fieldIdIndex: {}
3011
- };
3057
+ const metadataSources = [];
3012
3058
  const filePageUsage = /* @__PURE__ */ new Map();
3013
3059
  for (const file of state.files) {
3014
3060
  for (const page of file.pages) {
@@ -3033,28 +3079,12 @@ var PageAssembler = React7.forwardRef(
3033
3079
  }
3034
3080
  console.log("[PageAssembler] Page mapping:", Object.fromEntries(pageIdToNewPageNumber));
3035
3081
  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
- }
3082
+ const sourceMetadata = sourcePdf ? getSigniphiMetadata(sourcePdf) : null;
3083
+ if (sourcePdf && !sourceMetadata) {
3084
+ console.warn(`[PageAssembler] No readable SigniphiMetadata in "${fileName}"`);
3057
3085
  }
3086
+ const placements = [];
3087
+ metadataSources.push({ metadata: sourceMetadata, fields: placements });
3058
3088
  console.log(`[PageAssembler] Extracting fields from "${fileName}"`);
3059
3089
  const extractedFields = await extractFieldsFromPdf(pdfBytes2);
3060
3090
  console.log(`[PageAssembler] Found ${extractedFields.length} fields in "${fileName}"`);
@@ -3088,13 +3118,7 @@ var PageAssembler = React7.forwardRef(
3088
3118
  };
3089
3119
  allExtractedFields.push(trackedField);
3090
3120
  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
- }
3121
+ placements.push({ name: field.name, page: newPageNumber });
3098
3122
  }
3099
3123
  }
3100
3124
  }
@@ -3172,21 +3196,15 @@ var PageAssembler = React7.forwardRef(
3172
3196
  }
3173
3197
  }
3174
3198
  }
3175
- if (Object.keys(mergedMetadata.fields).length > 0) {
3199
+ const mergedMetadata = mergeSourceMetadata(metadataSources);
3200
+ if (mergedMetadata) {
3176
3201
  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
- }
3202
+ setSigniphiMetadata(combinedDoc, mergedMetadata);
3187
3203
  } catch (metadataError) {
3188
3204
  console.warn("Could not write merged metadata:", metadataError);
3189
3205
  }
3206
+ } else {
3207
+ console.warn("[PageAssembler] No source metadata survived the merge; the assembled document will read as unprepared downstream");
3190
3208
  }
3191
3209
  let pdfBytes = await combinedDoc.save();
3192
3210
  if (allExtractedFields.length > 0) {