@signiphi/page-assembly 0.2.0-beta.12 → 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;
@@ -1972,6 +2022,7 @@ function mapFieldPositionsAfterAssembly(fields, pageMapping) {
1972
2022
  return pageMapping.has(mappingKey) || pageMapping.size === 0;
1973
2023
  });
1974
2024
  }
2025
+ var VISIBLE_PAGE_ROWS = 2;
1975
2026
  var isMultiSelectModifier = (event) => {
1976
2027
  return event.ctrlKey || event.metaKey;
1977
2028
  };
@@ -2298,21 +2349,48 @@ var ReorderablePageGrid = ({
2298
2349
  newPages.splice(hoverIndex, 0, draggedPage);
2299
2350
  onReorderPages(file.id, newPages);
2300
2351
  }, [file.id, file.pages, onReorderPages]);
2301
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4", children: file.pages.map((page, index) => /* @__PURE__ */ jsxRuntime.jsx(
2302
- DraggablePageThumbnail,
2352
+ const gridRef = React7.useRef(null);
2353
+ const [maxHeight, setMaxHeight] = React7.useState(void 0);
2354
+ React7.useEffect(() => {
2355
+ const grid = gridRef.current;
2356
+ if (!grid || typeof ResizeObserver === "undefined") return;
2357
+ const measure = () => {
2358
+ const firstThumbnail = grid.firstElementChild;
2359
+ if (!firstThumbnail) return;
2360
+ const rowHeight = firstThumbnail.getBoundingClientRect().height;
2361
+ if (rowHeight <= 0) return;
2362
+ const rowGap = parseFloat(getComputedStyle(grid).rowGap) || 0;
2363
+ setMaxHeight(Math.ceil(rowHeight * VISIBLE_PAGE_ROWS + rowGap * (VISIBLE_PAGE_ROWS - 1)));
2364
+ };
2365
+ measure();
2366
+ const observer = new ResizeObserver(measure);
2367
+ observer.observe(grid);
2368
+ if (grid.firstElementChild) observer.observe(grid.firstElementChild);
2369
+ return () => observer.disconnect();
2370
+ }, [file.pages]);
2371
+ return /* @__PURE__ */ jsxRuntime.jsx(
2372
+ "div",
2303
2373
  {
2304
- page,
2305
- fileId: file.id,
2306
- index,
2307
- isSelected: selectedPageIds.includes(page.id),
2308
- onSingleSelect: onSingleSelectPage,
2309
- onToggleSelect: onTogglePageSelect,
2310
- onRangeSelect: onRangeSelectPage,
2311
- movePageInFile,
2312
- isDraggingPageRef
2313
- },
2314
- page.id
2315
- )) });
2374
+ ref: gridRef,
2375
+ className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4 overflow-y-auto overflow-x-hidden",
2376
+ style: { maxHeight },
2377
+ children: file.pages.map((page, index) => /* @__PURE__ */ jsxRuntime.jsx(
2378
+ DraggablePageThumbnail,
2379
+ {
2380
+ page,
2381
+ fileId: file.id,
2382
+ index,
2383
+ isSelected: selectedPageIds.includes(page.id),
2384
+ onSingleSelect: onSingleSelectPage,
2385
+ onToggleSelect: onTogglePageSelect,
2386
+ onRangeSelect: onRangeSelectPage,
2387
+ movePageInFile,
2388
+ isDraggingPageRef
2389
+ },
2390
+ page.id
2391
+ ))
2392
+ }
2393
+ );
2316
2394
  };
2317
2395
  var FileDropZone = ({
2318
2396
  targetIndex,
@@ -2897,7 +2975,7 @@ var PageAssembler = React7.forwardRef(
2897
2975
  setIsLoading(true);
2898
2976
  setProcessingStatus("Assembling PDF...");
2899
2977
  try {
2900
- const { PDFDocument: PDFDocument5, PDFName: PDFName4, PDFString: PDFString4 } = await import('pdf-lib');
2978
+ const { PDFDocument: PDFDocument5, PDFName: PDFName4 } = await import('pdf-lib');
2901
2979
  const loadedPdfs = /* @__PURE__ */ new Map();
2902
2980
  const fileMap = /* @__PURE__ */ new Map();
2903
2981
  const pdfFiles = state.files.filter((f) => f.type === "pdf" && f.originalBytes?.length > 0);
@@ -2976,11 +3054,7 @@ var PageAssembler = React7.forwardRef(
2976
3054
  }
2977
3055
  console.log("[PageAssembler] Starting multi-PDF assembly with field preservation");
2978
3056
  const allExtractedFields = [];
2979
- const mergedMetadata = {
2980
- version: "1.0",
2981
- fields: {},
2982
- fieldIdIndex: {}
2983
- };
3057
+ const metadataSources = [];
2984
3058
  const filePageUsage = /* @__PURE__ */ new Map();
2985
3059
  for (const file of state.files) {
2986
3060
  for (const page of file.pages) {
@@ -3005,28 +3079,12 @@ var PageAssembler = React7.forwardRef(
3005
3079
  }
3006
3080
  console.log("[PageAssembler] Page mapping:", Object.fromEntries(pageIdToNewPageNumber));
3007
3081
  const extractAndMapFields = async (fileId, fileName, pdfBytes2, sourcePdf) => {
3008
- let sourceMetadata = null;
3009
- if (sourcePdf) {
3010
- try {
3011
- const sourceInfoRef = sourcePdf.context.trailerInfo.Info;
3012
- if (sourceInfoRef) {
3013
- const sourceInfoDict = sourcePdf.context.lookup(sourceInfoRef);
3014
- if (sourceInfoDict && typeof sourceInfoDict.get === "function") {
3015
- const metadataObj = sourceInfoDict.get(PDFName4.of("SigniphiMetadata"));
3016
- if (metadataObj) {
3017
- const metadataStr = metadataObj.toString();
3018
- let jsonStr = metadataStr;
3019
- if (metadataStr.startsWith("(") && metadataStr.endsWith(")")) {
3020
- jsonStr = metadataStr.slice(1, -1);
3021
- }
3022
- sourceMetadata = JSON.parse(jsonStr);
3023
- }
3024
- }
3025
- }
3026
- } catch (metadataError) {
3027
- console.warn(`Could not read metadata from PDF ${fileName}:`, metadataError);
3028
- }
3082
+ const sourceMetadata = sourcePdf ? getSigniphiMetadata(sourcePdf) : null;
3083
+ if (sourcePdf && !sourceMetadata) {
3084
+ console.warn(`[PageAssembler] No readable SigniphiMetadata in "${fileName}"`);
3029
3085
  }
3086
+ const placements = [];
3087
+ metadataSources.push({ metadata: sourceMetadata, fields: placements });
3030
3088
  console.log(`[PageAssembler] Extracting fields from "${fileName}"`);
3031
3089
  const extractedFields = await extractFieldsFromPdf(pdfBytes2);
3032
3090
  console.log(`[PageAssembler] Found ${extractedFields.length} fields in "${fileName}"`);
@@ -3060,13 +3118,7 @@ var PageAssembler = React7.forwardRef(
3060
3118
  };
3061
3119
  allExtractedFields.push(trackedField);
3062
3120
  console.log(`[PageAssembler] Mapped field "${field.name}" from page ${originalPage} to page ${newPageNumber}`);
3063
- const metadataKey = `${field.name}_p${newPageNumber}`;
3064
- if (sourceMetadata?.fields?.[field.name]) {
3065
- mergedMetadata.fields[metadataKey] = sourceMetadata.fields[field.name];
3066
- }
3067
- if (sourceMetadata?.fieldIdIndex?.[field.name]) {
3068
- mergedMetadata.fieldIdIndex[metadataKey] = sourceMetadata.fieldIdIndex[field.name];
3069
- }
3121
+ placements.push({ name: field.name, page: newPageNumber });
3070
3122
  }
3071
3123
  }
3072
3124
  }
@@ -3144,21 +3196,15 @@ var PageAssembler = React7.forwardRef(
3144
3196
  }
3145
3197
  }
3146
3198
  }
3147
- if (Object.keys(mergedMetadata.fields).length > 0) {
3199
+ const mergedMetadata = mergeSourceMetadata(metadataSources);
3200
+ if (mergedMetadata) {
3148
3201
  try {
3149
- const newInfoRef = combinedDoc.context.trailerInfo.Info;
3150
- if (newInfoRef) {
3151
- const newInfoDict = combinedDoc.context.lookup(newInfoRef);
3152
- if (newInfoDict && typeof newInfoDict.set === "function") {
3153
- newInfoDict.set(
3154
- PDFName4.of("SigniphiMetadata"),
3155
- PDFString4.of(JSON.stringify(mergedMetadata))
3156
- );
3157
- }
3158
- }
3202
+ setSigniphiMetadata(combinedDoc, mergedMetadata);
3159
3203
  } catch (metadataError) {
3160
3204
  console.warn("Could not write merged metadata:", metadataError);
3161
3205
  }
3206
+ } else {
3207
+ console.warn("[PageAssembler] No source metadata survived the merge; the assembled document will read as unprepared downstream");
3162
3208
  }
3163
3209
  let pdfBytes = await combinedDoc.save();
3164
3210
  if (allExtractedFields.length > 0) {