@measurequick/measurequick-report-generator 1.5.235 → 1.5.236

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@measurequick/measurequick-report-generator",
3
- "version": "1.5.235",
3
+ "version": "1.5.236",
4
4
  "description": "Generates PDF documents for various measureQuick applications.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -12,7 +12,8 @@
12
12
  "jspdf": "^1.3.5",
13
13
  "pako": "^2.0.4",
14
14
  "pdf-lib": "^1.16.0",
15
- "prettier": "^3.5.3"
15
+ "prettier": "^3.5.3",
16
+ "qrcode": "^1.5.4"
16
17
  },
17
18
  "keywords": [
18
19
  "measurequick"
@@ -7,6 +7,7 @@ import * as photos4nci from '../base-64/photos4nci.js';
7
7
  import * as photos6nci from '../base-64/photos6nci.js';
8
8
  import * as base64 from '../base-64/icons.js';
9
9
  import * as util from '../util.js';
10
+ import { getButtonRect, drawVideoQrOverlay } from './video-qr-helper.js';
10
11
 
11
12
  export async function getReport(payload) {
12
13
  try {
@@ -18,9 +19,9 @@ export async function getReport(payload) {
18
19
  console.log('payload',payload);
19
20
  let nci = payload&&payload.test&&payload.test.ductScreeningType;
20
21
  console.log('nci',nci);
21
-
22
+
22
23
  page = nci ? photos6nci : page;
23
-
24
+
24
25
  if (maxPhotosPerPage == 4) {
25
26
  page = nci ? photos4nci : photos4;
26
27
  pSize = "Mid";
@@ -29,6 +30,9 @@ export async function getReport(payload) {
29
30
  pSize = "Large";
30
31
  }
31
32
 
33
+ // Track video overlays to draw after flattening
34
+ let videoOverlays = [];
35
+
32
36
  // Process All Photos
33
37
  let docIndex = 0;
34
38
  let photos = payload.photos.project;
@@ -63,7 +67,17 @@ export async function getReport(payload) {
63
67
  if (photo.base64 && photo.base64.includes("image/jpeg")) imageToSet = await docs[docIndex].embedJpg(util._base64ToArrayBuffer(photo.base64));
64
68
  else if (photo.base64 && photo.base64.includes("image/png")) imageToSet = await docs[docIndex].embedPng(util._base64ToArrayBuffer(photo.base64));
65
69
  }
66
- if (imageToSet) forms[docIndex].getButton(`photo${pSize}${photoPosition}`).setImage(imageToSet);
70
+ let fieldName = `photo${pSize}${photoPosition}`;
71
+ if (imageToSet) forms[docIndex].getButton(fieldName).setImage(imageToSet);
72
+
73
+ // Track video for QR overlay
74
+ if (photo.isVideo && photo.viewerUrl) {
75
+ let rect = getButtonRect(forms[docIndex], fieldName);
76
+ if (rect) {
77
+ videoOverlays.push({ docIndex: docIndex, rect: rect, viewerUrl: photo.viewerUrl });
78
+ }
79
+ }
80
+
67
81
  let caption = photo.section;
68
82
  if (prevSection == currSection) caption += photo.notes;
69
83
  forms[docIndex].getTextField(`Notes${photoPosition}`).setText(caption);
@@ -76,6 +90,13 @@ export async function getReport(payload) {
76
90
 
77
91
  // Prepare Deliverable
78
92
  forms.forEach(form => { form.flatten() });
93
+
94
+ // Draw video QR overlays on flattened pages
95
+ for (let overlay of videoOverlays) {
96
+ let docPage = docs[overlay.docIndex].getPage(0);
97
+ await drawVideoQrOverlay(docs[overlay.docIndex], docPage, overlay.rect, overlay.viewerUrl);
98
+ }
99
+
79
100
  let pdfDoc = await PDFDocument.create();
80
101
  for (let i = 0; i < docs.length; i++) {
81
102
  const [nextPage] = await pdfDoc.copyPages(docs[i], [0]);
@@ -7,6 +7,7 @@ import * as photos4nci from '../base-64/photos4nci.js';
7
7
  import * as photos6nci from '../base-64/photos6nci.js';
8
8
  import * as base64 from '../base-64/icons.js';
9
9
  import * as util from '../util.js';
10
+ import { getButtonRect, drawVideoQrOverlay } from './video-qr-helper.js';
10
11
 
11
12
  // Check if project is CoolMaxx or HeatMaxx (NCI branded)
12
13
  function isNCIProject(payload) {
@@ -50,6 +51,9 @@ export async function getReport(payload) {
50
51
  }
51
52
  }
52
53
 
54
+ // Track video overlays to draw after flattening
55
+ let videoOverlays = []; // { docIndex, rect, s3ReferenceId }
56
+
53
57
  // Process All Photos
54
58
  let canProcessReport = false;
55
59
  let previousPhotoSectionName;
@@ -95,7 +99,17 @@ export async function getReport(payload) {
95
99
  else if (photo.base64.includes("image/png")) imageToSet = await docs[docIndex].embedPng(util._base64ToArrayBuffer(photo.base64));
96
100
  else imageToSet = await docs[docIndex].embedJpg(util._base64ToArrayBuffer(photo.base64));
97
101
  }
98
- if (imageToSet) forms[docIndex].getButton(`photo${pSize}${photoPosition}`).setImage(imageToSet);
102
+ let fieldName = `photo${pSize}${photoPosition}`;
103
+ if (imageToSet) forms[docIndex].getButton(fieldName).setImage(imageToSet);
104
+
105
+ // Track video for QR overlay
106
+ if (photo.isVideo && photo.viewerUrl) {
107
+ let rect = getButtonRect(forms[docIndex], fieldName);
108
+ if (rect) {
109
+ videoOverlays.push({ docIndex: docIndex, rect: rect, viewerUrl: photo.viewerUrl });
110
+ }
111
+ }
112
+
99
113
  let photoSubText = photoSection.name;
100
114
  let isFirstPhotoOfSection = previousPhotoSectionName !== photoSectionName;
101
115
  if (isFirstPhotoOfSection && photoSection.hasNotes && photoSection.notes) photoSubText += `: ${photoSection.notes}`;
@@ -160,7 +174,17 @@ export async function getReport(payload) {
160
174
  }
161
175
  //else imageToSet = await docs[docIndex].embedJpg(util._base64ToArrayBuffer(photo.base64));
162
176
  }
163
- if (imageToSet) forms[docIndex].getButton(`photo${pSize}${photoPosition}`).setImage(imageToSet);
177
+ let fieldName = `photo${pSize}${photoPosition}`;
178
+ if (imageToSet) forms[docIndex].getButton(fieldName).setImage(imageToSet);
179
+
180
+ // Track video for QR overlay
181
+ if (photo.isVideo && photo.viewerUrl) {
182
+ let rect = getButtonRect(forms[docIndex], fieldName);
183
+ if (rect) {
184
+ videoOverlays.push({ docIndex: docIndex, rect: rect, viewerUrl: photo.viewerUrl });
185
+ }
186
+ }
187
+
164
188
  let photoSubText = photoSection.name;
165
189
  let isFirstPhotoOfSection = previousPhotoSectionName !== photoSectionName;
166
190
  if (isFirstPhotoOfSection && photoSection.hasNotes && photoSection.notes) photoSubText += `: ${photoSection.notes}`;
@@ -178,6 +202,13 @@ export async function getReport(payload) {
178
202
  // Prepare Deliverable
179
203
  if (canProcessReport) {
180
204
  forms.forEach(form => { form.flatten() });
205
+
206
+ // Draw video QR overlays on flattened pages
207
+ for (let overlay of videoOverlays) {
208
+ let docPage = docs[overlay.docIndex].getPage(0);
209
+ await drawVideoQrOverlay(docs[overlay.docIndex], docPage, overlay.rect, overlay.viewerUrl);
210
+ }
211
+
181
212
  let pdfDoc = await PDFDocument.create();
182
213
  for (let i = 0; i < docs.length; i++) {
183
214
  const [nextPage] = await pdfDoc.copyPages(docs[i], [0]);
@@ -0,0 +1,84 @@
1
+ import QRCode from 'qrcode';
2
+ import { rgb } from 'pdf-lib';
3
+ import * as util from '../util.js';
4
+
5
+ /**
6
+ * Generate QR code as PNG bytes for a viewer URL
7
+ */
8
+ export async function generateQrPngBytes(viewerUrl) {
9
+ const dataUrl = await QRCode.toDataURL(viewerUrl, {
10
+ width: 200,
11
+ margin: 1,
12
+ color: { dark: '#000000', light: '#ffffff' }
13
+ });
14
+ return util._base64ToArrayBuffer(dataUrl);
15
+ }
16
+
17
+ /**
18
+ * Get the rectangle of a form button field
19
+ */
20
+ export function getButtonRect(form, fieldName) {
21
+ try {
22
+ const button = form.getButton(fieldName);
23
+ const widgets = button.acroField.getWidgets();
24
+ if (widgets.length > 0) {
25
+ return widgets[0].getRectangle();
26
+ }
27
+ } catch (e) {
28
+ // field not found
29
+ }
30
+ return null;
31
+ }
32
+
33
+ /**
34
+ * Draw QR code overlay with "VIEW VIDEO" text on a PDF page
35
+ * Centered in the given rectangle area
36
+ */
37
+ export async function drawVideoQrOverlay(doc, page, rect, viewerUrl) {
38
+ if (!rect || !viewerUrl) return;
39
+
40
+ const qrBytes = await generateQrPngBytes(viewerUrl);
41
+ if (!qrBytes) return;
42
+
43
+ const qrImage = await doc.embedPng(qrBytes);
44
+
45
+ // Size the QR code to ~40% of the photo area width, min 50, max 100
46
+ let qrSize = Math.min(Math.max(rect.width * 0.4, 50), 100);
47
+ const padding = 8;
48
+ const textHeight = 12;
49
+ const totalHeight = qrSize + padding * 2 + textHeight + 4;
50
+ const totalWidth = qrSize + padding * 2;
51
+
52
+ // Center the overlay in the photo area
53
+ const boxX = rect.x + (rect.width - totalWidth) / 2;
54
+ const boxY = rect.y + (rect.height - totalHeight) / 2;
55
+
56
+ // White background box
57
+ page.drawRectangle({
58
+ x: boxX,
59
+ y: boxY,
60
+ width: totalWidth,
61
+ height: totalHeight,
62
+ color: rgb(1, 1, 1),
63
+ borderColor: rgb(0.8, 0.8, 0.8),
64
+ borderWidth: 0.5
65
+ });
66
+
67
+ // QR code image
68
+ page.drawImage(qrImage, {
69
+ x: boxX + padding,
70
+ y: boxY + textHeight + 4 + padding,
71
+ width: qrSize,
72
+ height: qrSize
73
+ });
74
+
75
+ // "VIEW VIDEO" text centered below QR
76
+ const fontSize = 8;
77
+ const textWidth = fontSize * 6.5; // approximate width of "VIEW VIDEO"
78
+ page.drawText('VIEW VIDEO', {
79
+ x: boxX + (totalWidth - textWidth) / 2,
80
+ y: boxY + padding,
81
+ size: fontSize,
82
+ color: rgb(0, 0, 0)
83
+ });
84
+ }