@mrfylke/nsr-barcode-generator 1.2.0 → 1.3.0

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.
@@ -37,14 +37,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.generatePdfsForStopPlaces = generatePdfsForStopPlaces;
40
+ const node_fs_1 = require("node:fs");
41
+ const node_path_1 = require("node:path");
40
42
  const pdfkit_1 = __importDefault(require("pdfkit"));
41
- const fs_1 = require("fs");
42
- const path_1 = require("path");
43
- const fs_2 = require("fs");
44
43
  const QRCode = __importStar(require("qrcode"));
44
+ const assets_1 = require("./assets");
45
45
  const enturApi_1 = require("./enturApi");
46
46
  const fontLoader_1 = require("./fontLoader");
47
- const assets_1 = require("./assets");
48
47
  async function generatePdfsForStopPlaces(requests, options) {
49
48
  const { outputDirectory, overwrite = false, onProgress } = options;
50
49
  await ensureDirectoryExists(outputDirectory);
@@ -67,7 +66,26 @@ async function generatePdfsForStopPlaces(requests, options) {
67
66
  let fetchedInfos = [];
68
67
  if (idsNeedingFetch.length > 0) {
69
68
  const fetchStopPlaces = options.stopPlaceFetcher ??
70
- ((idsToFetch) => enturApi_1.enturApi.getMultipleStopPlaces(idsToFetch));
69
+ ((idsToFetch) => enturApi_1.enturApi.getMultipleStopPlaces(idsToFetch, {
70
+ ...(onProgress
71
+ ? {
72
+ onProgress: (event) => onProgress({
73
+ type: "data-fetching",
74
+ current: event.current,
75
+ total: event.total,
76
+ nsrId: event.nsrId,
77
+ status: event.status,
78
+ ...(event.error ? { error: event.error } : {}),
79
+ }),
80
+ onBatchComplete: (event) => onProgress({
81
+ type: "data-fetching-batch",
82
+ batchNumber: event.batchNumber,
83
+ totalBatches: event.totalBatches,
84
+ delayMs: event.delayMs,
85
+ }),
86
+ }
87
+ : {}),
88
+ }));
71
89
  fetchedInfos = await fetchStopPlaces(idsNeedingFetch);
72
90
  }
73
91
  const fetchedById = new Map(idsNeedingFetch.map((id, index) => [id, fetchedInfos[index] ?? null]));
@@ -95,10 +113,11 @@ async function generatePdfsForStopPlaces(requests, options) {
95
113
  throw new Error(`Failed to resolve stop place metadata for "${id}"`);
96
114
  }
97
115
  const filename = generateSafeFilename(id, stopPlaceInfo.name);
98
- const outputPath = (0, path_1.join)(outputDirectory, `${filename}.pdf`);
116
+ const outputPath = (0, node_path_1.join)(outputDirectory, `${filename}.pdf`);
99
117
  if (!overwrite && (await pathExists(outputPath))) {
100
118
  skipped.push({ nsrId: id, outputPath });
101
119
  onProgress?.({
120
+ type: "generation",
102
121
  current,
103
122
  total,
104
123
  nsrId: id,
@@ -110,6 +129,7 @@ async function generatePdfsForStopPlaces(requests, options) {
110
129
  await generateSinglePdf(id, outputPath, stopPlaceInfo, options);
111
130
  generated.push({ nsrId: id, outputPath });
112
131
  onProgress?.({
132
+ type: "generation",
113
133
  current,
114
134
  total,
115
135
  nsrId: id,
@@ -120,13 +140,20 @@ async function generatePdfsForStopPlaces(requests, options) {
120
140
  catch (error) {
121
141
  const message = error instanceof Error ? error.message : "Unknown error";
122
142
  failed.push({ nsrId: id, error: message });
123
- onProgress?.({ current, total, nsrId: id, status: "error", error: message });
143
+ onProgress?.({
144
+ type: "generation",
145
+ current,
146
+ total,
147
+ nsrId: id,
148
+ status: "error",
149
+ error: message,
150
+ });
124
151
  }
125
152
  }
126
153
  return {
127
154
  generatedFiles: generated.map((item) => item.outputPath),
128
155
  totalGenerated: generated.length,
129
- outputDirectory: (0, path_1.resolve)(outputDirectory),
156
+ outputDirectory: (0, node_path_1.resolve)(outputDirectory),
130
157
  generated,
131
158
  skipped,
132
159
  failed,
@@ -145,337 +172,338 @@ function isValidAbsoluteHttpUrl(value) {
145
172
  }
146
173
  }
147
174
  async function generateSinglePdf(id, outputPath, stopPlaceInfo, options) {
148
- return new Promise(async (resolvePromise, reject) => {
175
+ let stream;
176
+ try {
177
+ const fonts = await fontLoader_1.fontLoader.loadPoppins(options?.assetsDirectory);
178
+ const orientation = options?.orientation || "landscape";
179
+ const format = options?.format || "A4";
180
+ const doc = new pdfkit_1.default({
181
+ size: format,
182
+ layout: orientation,
183
+ margins: {
184
+ top: 50,
185
+ bottom: 50,
186
+ left: 50,
187
+ right: 50,
188
+ },
189
+ });
190
+ let poppinsRegular = "Helvetica";
191
+ let poppinsBold = "Helvetica-Bold";
149
192
  try {
150
- const fonts = await fontLoader_1.fontLoader.loadPoppins(options?.assetsDirectory);
151
- const orientation = options?.orientation || "landscape";
152
- const format = options?.format || "A4";
153
- const doc = new pdfkit_1.default({
154
- size: format,
155
- layout: orientation,
156
- margins: {
157
- top: 50,
158
- bottom: 50,
159
- left: 50,
160
- right: 50,
161
- },
162
- });
163
- let poppinsRegular = "Helvetica";
164
- let poppinsBold = "Helvetica-Bold";
165
- try {
166
- if (fonts.regular !== "Helvetica") {
167
- doc.registerFont("Poppins-Regular", fonts.regular);
168
- poppinsRegular = "Poppins-Regular";
169
- }
170
- if (fonts.bold !== "Helvetica-Bold") {
171
- doc.registerFont("Poppins-Bold", fonts.bold);
172
- poppinsBold = "Poppins-Bold";
173
- }
193
+ if (fonts.regular !== "Helvetica") {
194
+ doc.registerFont("Poppins-Regular", fonts.regular);
195
+ poppinsRegular = "Poppins-Regular";
174
196
  }
175
- catch (fontError) {
176
- console.warn("Failed to register Poppins fonts, using fallback:", fontError);
197
+ if (fonts.bold !== "Helvetica-Bold") {
198
+ doc.registerFont("Poppins-Bold", fonts.bold);
199
+ poppinsBold = "Poppins-Bold";
177
200
  }
178
- const stream = (0, fs_2.createWriteStream)(outputPath);
179
- doc.pipe(stream);
180
- const pageWidth = doc.page.width;
181
- const pageHeight = doc.page.height;
182
- const styleConfig = options?.style || {};
183
- const headerFooterColor = styleConfig.headerFooterColor || "#1A4D75";
184
- const logoPath = styleConfig.logoPath ||
185
- (0, assets_1.resolveAssetPath)(options?.assetsDirectory, "images", "fram_mor_fylkeskommune_dark.png");
186
- const logoWidth = styleConfig.logoWidth || 105;
187
- const fallbackLogoText = styleConfig.fallbackLogoText || "FRAM";
188
- const fallbackLogoSubtext = styleConfig.fallbackLogoSubtext || "Møre og Romsdal fylkeskommune";
189
- const white = "#FFFFFF";
190
- const darkGray = "#333333";
191
- const borderRadius = 15;
192
- const borderMargin = 20;
193
- const boxWidth = pageWidth - borderMargin * 2;
194
- const boxHeight = pageHeight - borderMargin * 2;
195
- doc
196
- .lineWidth(0.25)
197
- .roundedRect(borderMargin, borderMargin, boxWidth, boxHeight, borderRadius)
198
- .stroke("#000000");
199
- const headerHeight = 80;
200
- doc
201
- .save()
202
- .roundedRect(borderMargin, borderMargin, boxWidth, boxHeight, borderRadius)
203
- .clip()
204
- .rect(borderMargin, borderMargin, boxWidth, headerHeight)
205
- .fill(headerFooterColor)
206
- .restore();
207
- const iconX = 80;
208
- const iconY = 60;
209
- const iconSize = 50;
210
- try {
211
- const transportModes = stopPlaceInfo.transportMode || [];
212
- let iconFileName = "Bus.png";
213
- const modes = Array.isArray(transportModes)
214
- ? transportModes
215
- : [transportModes];
216
- const modeString = modes.join(" ").toLowerCase();
217
- if (modeString.includes("water") || modeString.includes("boat")) {
218
- iconFileName = "Boat.png";
219
- }
220
- else if (modeString.includes("ferry")) {
221
- iconFileName = "Ferry.png";
222
- }
223
- else if (modeString.includes("bus")) {
224
- iconFileName = "Bus.png";
225
- }
226
- const iconPath = (0, assets_1.resolveAssetPath)(options?.assetsDirectory, "images", iconFileName);
227
- const iconBuffer = await fs_1.promises.readFile(iconPath);
228
- doc
229
- .lineWidth(3)
230
- .circle(iconX, iconY, iconSize / 2)
231
- .stroke(white)
232
- .circle(iconX, iconY, iconSize / 2)
233
- .stroke(white);
234
- const iconImageSize = iconSize * 0.6;
235
- const iconImageX = iconX - iconImageSize / 2;
236
- const iconImageY = iconY - iconImageSize / 2;
237
- doc.image(iconBuffer, iconImageX, iconImageY, {
238
- width: iconImageSize,
239
- height: iconImageSize,
240
- });
201
+ }
202
+ catch (fontError) {
203
+ console.warn("Failed to register Poppins fonts, using fallback:", fontError);
204
+ }
205
+ stream = (0, node_fs_1.createWriteStream)(outputPath);
206
+ doc.pipe(stream);
207
+ const pageWidth = doc.page.width;
208
+ const pageHeight = doc.page.height;
209
+ const styleConfig = options?.style || {};
210
+ const headerFooterColor = styleConfig.headerFooterColor || "#1A4D75";
211
+ const logoPath = styleConfig.logoPath ||
212
+ (0, assets_1.resolveAssetPath)(options?.assetsDirectory, "images", "fram_mor_fylkeskommune_dark.png");
213
+ const logoWidth = styleConfig.logoWidth || 105;
214
+ const fallbackLogoText = styleConfig.fallbackLogoText || "FRAM";
215
+ const fallbackLogoSubtext = styleConfig.fallbackLogoSubtext || "Møre og Romsdal fylkeskommune";
216
+ const white = "#FFFFFF";
217
+ const darkGray = "#333333";
218
+ const borderRadius = 15;
219
+ const borderMargin = 20;
220
+ const boxWidth = pageWidth - borderMargin * 2;
221
+ const boxHeight = pageHeight - borderMargin * 2;
222
+ doc
223
+ .lineWidth(0.25)
224
+ .roundedRect(borderMargin, borderMargin, boxWidth, boxHeight, borderRadius)
225
+ .stroke("#000000");
226
+ const headerHeight = 80;
227
+ doc
228
+ .save()
229
+ .roundedRect(borderMargin, borderMargin, boxWidth, boxHeight, borderRadius)
230
+ .clip()
231
+ .rect(borderMargin, borderMargin, boxWidth, headerHeight)
232
+ .fill(headerFooterColor)
233
+ .restore();
234
+ const iconX = 80;
235
+ const iconY = 60;
236
+ const iconSize = 50;
237
+ try {
238
+ const transportModes = stopPlaceInfo.transportMode || [];
239
+ let iconFileName = "Bus.png";
240
+ const modes = Array.isArray(transportModes)
241
+ ? transportModes
242
+ : [transportModes];
243
+ const modeString = modes.join(" ").toLowerCase();
244
+ if (modeString.includes("water") || modeString.includes("boat")) {
245
+ iconFileName = "Boat.png";
241
246
  }
242
- catch (error) {
243
- console.warn("Could not load transport mode icon:", error);
244
- doc
245
- .lineWidth(3)
246
- .circle(iconX, iconY, iconSize / 2)
247
- .stroke(white);
248
- doc
249
- .fillColor(white)
250
- .fontSize(20)
251
- .font(poppinsBold)
252
- .text("x", iconX - 10, iconY - 10);
247
+ else if (modeString.includes("ferry")) {
248
+ iconFileName = "Ferry.png";
249
+ }
250
+ else if (modeString.includes("bus")) {
251
+ iconFileName = "Bus.png";
253
252
  }
254
- const stopName = stopPlaceInfo.name || id;
253
+ const iconPath = (0, assets_1.resolveAssetPath)(options?.assetsDirectory, "images", iconFileName);
254
+ const iconBuffer = await node_fs_1.promises.readFile(iconPath);
255
+ doc
256
+ .lineWidth(3)
257
+ .circle(iconX, iconY, iconSize / 2)
258
+ .stroke(white)
259
+ .circle(iconX, iconY, iconSize / 2)
260
+ .stroke(white);
261
+ const iconImageSize = iconSize * 0.6;
262
+ const iconImageX = iconX - iconImageSize / 2;
263
+ const iconImageY = iconY - iconImageSize / 2;
264
+ doc.image(iconBuffer, iconImageX, iconImageY, {
265
+ width: iconImageSize,
266
+ height: iconImageSize,
267
+ });
268
+ }
269
+ catch (error) {
270
+ console.warn("Could not load transport mode icon:", error);
271
+ doc
272
+ .lineWidth(3)
273
+ .circle(iconX, iconY, iconSize / 2)
274
+ .stroke(white);
255
275
  doc
256
276
  .fillColor(white)
257
- .fontSize(36)
277
+ .fontSize(20)
258
278
  .font(poppinsBold)
259
- .text(stopName, 130, 35, {
260
- width: pageWidth - 200,
261
- align: "left",
262
- });
263
- const footerHeight = 80;
264
- const availableHeight = pageHeight - headerHeight - footerHeight;
265
- const contentHeight = orientation === "portrait" ? 400 : 310;
266
- const contentY = headerHeight + (availableHeight - contentHeight) / 2;
267
- let qrUrl;
268
- if (options?.generateQrUrl) {
269
- const qrContext = {
270
- id,
271
- name: stopPlaceInfo.name ?? id,
272
- };
273
- const candidate = options.generateQrUrl(qrContext);
274
- if (!isValidAbsoluteHttpUrl(candidate)) {
275
- throw new Error(`generateQrUrl returned an invalid URL for "${id}": ${JSON.stringify(candidate)}. Expected a non-empty absolute http(s) URL.`);
276
- }
277
- qrUrl = candidate;
278
- }
279
- else {
280
- qrUrl = `https://reise.frammr.no/departures/${encodeURIComponent(id)}?qr`;
281
- }
282
- const qrCodeDataURL = await QRCode.toDataURL(qrUrl, {
283
- width: 200,
284
- margin: 1,
285
- color: {
286
- dark: "#000000",
287
- light: "#FFFFFF",
288
- },
289
- });
290
- const base64Data = qrCodeDataURL.split(",")[1];
291
- let qrCodeBuffer = null;
292
- if (base64Data) {
293
- qrCodeBuffer = Buffer.from(base64Data, "base64");
294
- }
295
- if (orientation === "portrait") {
296
- const qrSize = 120;
297
- const topMargin = headerHeight + 20;
298
- const qrX = iconX - iconSize / 2;
299
- const qrY = topMargin;
300
- if (qrCodeBuffer) {
301
- doc.image(qrCodeBuffer, qrX, qrY, { width: qrSize, height: qrSize });
302
- }
303
- const rightColumnX = pageWidth - 350;
304
- const rightColumnStartY = topMargin;
305
- doc
306
- .fillColor(darkGray)
307
- .fontSize(24)
308
- .font(poppinsBold)
309
- .text("Når kjem bussen?", rightColumnX, rightColumnStartY);
310
- doc
311
- .fillColor(darkGray)
312
- .fontSize(14)
313
- .font(poppinsRegular)
314
- .text("Opne mobilkameraet ditt og hald", rightColumnX, rightColumnStartY + 40)
315
- .text("kameralinsa over QR-koden. Lenka", rightColumnX, rightColumnStartY + 60)
316
- .text("fører deg til reiseplanleggaren, og viser", rightColumnX, rightColumnStartY + 80)
317
- .text("busslinjer og avgangar frå haldeplassen", rightColumnX, rightColumnStartY + 100)
318
- .text("du står på.", rightColumnX, rightColumnStartY + 120);
319
- const englishStartY = rightColumnStartY + 160;
320
- doc
321
- .fillColor(darkGray)
322
- .fontSize(24)
323
- .font(poppinsBold)
324
- .text("When will the bus", rightColumnX, englishStartY)
325
- .text("arrive?", rightColumnX, englishStartY + 25);
326
- doc
327
- .fillColor(darkGray)
328
- .fontSize(14)
329
- .font(poppinsRegular)
330
- .text("Open your mobile camera and hold", rightColumnX, englishStartY + 60)
331
- .text("camera lens over the QR code. The link", rightColumnX, englishStartY + 80)
332
- .text("takes you to the travel planner, and", rightColumnX, englishStartY + 100)
333
- .text("shows bus lines and departures from the", rightColumnX, englishStartY + 120)
334
- .text("stop you are at.", rightColumnX, englishStartY + 140);
335
- const infoStartY = englishStartY + 180;
336
- doc
337
- .fillColor(darkGray)
338
- .fontSize(14)
339
- .font(poppinsBold)
340
- .text("Informasjon om bussavgangar", rightColumnX, infoStartY)
341
- .text("finn du också:", rightColumnX, infoStartY + 20);
342
- doc
343
- .fillColor(darkGray)
344
- .fontSize(14)
345
- .font(poppinsRegular)
346
- .text("Information about bus departures", rightColumnX, infoStartY + 40)
347
- .text("can also be found:", rightColumnX, infoStartY + 60);
348
- doc
349
- .fillColor(darkGray)
350
- .fontSize(14)
351
- .font(poppinsRegular)
352
- .text("• I appane / in the apps:", rightColumnX, infoStartY + 100);
353
- doc
354
- .font(poppinsBold)
355
- .text("FRAM / Entur", rightColumnX + 10, infoStartY + 120);
356
- doc
357
- .font(poppinsRegular)
358
- .text("• På nettsidene / on the websites:", rightColumnX, infoStartY + 155);
359
- doc
360
- .font(poppinsBold)
361
- .text("frammr.no / entur.no", rightColumnX + 10, infoStartY + 175);
279
+ .text("x", iconX - 10, iconY - 10);
280
+ }
281
+ const stopName = stopPlaceInfo.name || id;
282
+ doc
283
+ .fillColor(white)
284
+ .fontSize(36)
285
+ .font(poppinsBold)
286
+ .text(stopName, 130, 35, {
287
+ width: pageWidth - 200,
288
+ align: "left",
289
+ });
290
+ const footerHeight = 80;
291
+ const availableHeight = pageHeight - headerHeight - footerHeight;
292
+ const contentHeight = orientation === "portrait" ? 400 : 310;
293
+ const contentY = headerHeight + (availableHeight - contentHeight) / 2;
294
+ let qrUrl;
295
+ if (options?.generateQrUrl) {
296
+ const qrContext = {
297
+ id,
298
+ name: stopPlaceInfo.name ?? id,
299
+ };
300
+ const candidate = options.generateQrUrl(qrContext);
301
+ if (!isValidAbsoluteHttpUrl(candidate)) {
302
+ throw new Error(`generateQrUrl returned an invalid URL for "${id}": ${JSON.stringify(candidate)}. Expected a non-empty absolute http(s) URL.`);
362
303
  }
363
- else {
364
- const qrSize = 120;
365
- const qrX = iconX - iconSize / 2;
366
- const qrY = contentY;
367
- if (qrCodeBuffer) {
368
- doc.image(qrCodeBuffer, qrX, qrY, { width: qrSize, height: qrSize });
369
- }
370
- const textStartX = qrX + qrSize + 20;
371
- const rightColumnX = pageWidth - 320;
372
- doc
373
- .fillColor(darkGray)
374
- .fontSize(24)
375
- .font(poppinsBold)
376
- .text("Når kjem bussen?", textStartX, contentY);
377
- doc
378
- .fillColor(darkGray)
379
- .fontSize(14)
380
- .font(poppinsRegular)
381
- .text("Opne mobilkameraet ditt og hald", textStartX, contentY + 40)
382
- .text("kameralinsa over QR-koden. Lenka", textStartX, contentY + 60)
383
- .text("fører deg til reiseplanleggaren, og viser", textStartX, contentY + 80)
384
- .text("busslinjer og avgangar frå haldeplassen", textStartX, contentY + 100)
385
- .text("du står på.", textStartX, contentY + 120);
386
- doc
387
- .fillColor(darkGray)
388
- .fontSize(24)
389
- .font(poppinsBold)
390
- .text("When will the bus", textStartX, contentY + 160)
391
- .text("arrive?", textStartX, contentY + 185);
392
- doc
393
- .fillColor(darkGray)
394
- .fontSize(14)
395
- .font(poppinsRegular)
396
- .text("Open your mobile camera and hold", textStartX, contentY + 220)
397
- .text("camera lens over the QR code. The link", textStartX, contentY + 240)
398
- .text("takes you to the travel planner, and", textStartX, contentY + 260)
399
- .text("shows bus lines and departures from the", textStartX, contentY + 280)
400
- .text("stop you are at.", textStartX, contentY + 300);
401
- const separatorX = rightColumnX - 22;
402
- doc
403
- .lineWidth(0.25)
404
- .moveTo(separatorX, contentY)
405
- .lineTo(separatorX, contentY + 320)
406
- .stroke("#000000");
407
- const rightColumnStartY = contentY + 40;
408
- doc
409
- .fillColor(darkGray)
410
- .fontSize(14)
411
- .font(poppinsBold)
412
- .text("Informasjon om bussavgangar", rightColumnX, rightColumnStartY)
413
- .text("finn du også:", rightColumnX, rightColumnStartY + 20);
414
- doc
415
- .fillColor(darkGray)
416
- .fontSize(14)
417
- .font(poppinsRegular)
418
- .text("Information about bus departures", rightColumnX, rightColumnStartY + 40)
419
- .text("can also be found:", rightColumnX, rightColumnStartY + 60);
420
- doc
421
- .fillColor(darkGray)
422
- .fontSize(14)
423
- .font(poppinsRegular)
424
- .text("• I appane / in the apps:", rightColumnX, rightColumnStartY + 100);
425
- doc
426
- .font(poppinsBold)
427
- .text("FRAM / Entur", rightColumnX + 10, rightColumnStartY + 120);
428
- doc
429
- .font(poppinsRegular)
430
- .text("• På nettsidene / on the websites:", rightColumnX, rightColumnStartY + 155);
431
- doc
432
- .font(poppinsBold)
433
- .text("frammr.no / entur.no", rightColumnX + 10, rightColumnStartY + 175);
304
+ qrUrl = candidate;
305
+ }
306
+ else {
307
+ qrUrl = `https://reise.frammr.no/departures/${encodeURIComponent(id)}?qr`;
308
+ }
309
+ const qrCodeDataURL = await QRCode.toDataURL(qrUrl, {
310
+ width: 200,
311
+ margin: 1,
312
+ color: {
313
+ dark: "#000000",
314
+ light: "#FFFFFF",
315
+ },
316
+ });
317
+ const base64Data = qrCodeDataURL.split(",")[1];
318
+ let qrCodeBuffer = null;
319
+ if (base64Data) {
320
+ qrCodeBuffer = Buffer.from(base64Data, "base64");
321
+ }
322
+ if (orientation === "portrait") {
323
+ const qrSize = 120;
324
+ const topMargin = headerHeight + 20;
325
+ const qrX = iconX - iconSize / 2;
326
+ const qrY = topMargin;
327
+ if (qrCodeBuffer) {
328
+ doc.image(qrCodeBuffer, qrX, qrY, { width: qrSize, height: qrSize });
434
329
  }
435
- const footerY = pageHeight - footerHeight;
330
+ const rightColumnX = pageWidth - 350;
331
+ const rightColumnStartY = topMargin;
436
332
  doc
437
- .save()
438
- .roundedRect(borderMargin, borderMargin, boxWidth, boxHeight, borderRadius)
439
- .clip()
440
- .rect(borderMargin, footerY, boxWidth, footerHeight)
441
- .fill(headerFooterColor)
442
- .restore();
443
- try {
444
- const logoBuffer = await fs_1.promises.readFile(logoPath);
445
- const logoHeight = Math.round(logoWidth * 0.27);
446
- const logoX = pageWidth - borderMargin - logoWidth - 20;
447
- const logoY = footerY + footerHeight - logoHeight - 35;
448
- doc.image(logoBuffer, logoX, logoY, {
449
- width: logoWidth,
450
- height: logoHeight,
451
- });
452
- }
453
- catch (error) {
454
- console.warn("Could not load logo image:", error);
455
- doc
456
- .fillColor(white)
457
- .fontSize(24)
458
- .font(poppinsBold)
459
- .text(fallbackLogoText, pageWidth - 150, footerY + 25);
460
- if (fallbackLogoSubtext) {
461
- doc
462
- .fillColor(white)
463
- .fontSize(10)
464
- .font(poppinsRegular)
465
- .text(fallbackLogoSubtext, pageWidth - 220, footerY + 55);
466
- }
333
+ .fillColor(darkGray)
334
+ .fontSize(24)
335
+ .font(poppinsBold)
336
+ .text("Når kjem bussen?", rightColumnX, rightColumnStartY);
337
+ doc
338
+ .fillColor(darkGray)
339
+ .fontSize(14)
340
+ .font(poppinsRegular)
341
+ .text("Opne mobilkameraet ditt og hald", rightColumnX, rightColumnStartY + 40)
342
+ .text("kameralinsa over QR-koden. Lenka", rightColumnX, rightColumnStartY + 60)
343
+ .text("fører deg til reiseplanleggaren, og viser", rightColumnX, rightColumnStartY + 80)
344
+ .text("busslinjer og avgangar frå haldeplassen", rightColumnX, rightColumnStartY + 100)
345
+ .text("du står på.", rightColumnX, rightColumnStartY + 120);
346
+ const englishStartY = rightColumnStartY + 160;
347
+ doc
348
+ .fillColor(darkGray)
349
+ .fontSize(24)
350
+ .font(poppinsBold)
351
+ .text("When will the bus", rightColumnX, englishStartY)
352
+ .text("arrive?", rightColumnX, englishStartY + 25);
353
+ doc
354
+ .fillColor(darkGray)
355
+ .fontSize(14)
356
+ .font(poppinsRegular)
357
+ .text("Open your mobile camera and hold", rightColumnX, englishStartY + 60)
358
+ .text("camera lens over the QR code. The link", rightColumnX, englishStartY + 80)
359
+ .text("takes you to the travel planner, and", rightColumnX, englishStartY + 100)
360
+ .text("shows bus lines and departures from the", rightColumnX, englishStartY + 120)
361
+ .text("stop you are at.", rightColumnX, englishStartY + 140);
362
+ const infoStartY = englishStartY + 180;
363
+ doc
364
+ .fillColor(darkGray)
365
+ .fontSize(14)
366
+ .font(poppinsBold)
367
+ .text("Informasjon om bussavgangar", rightColumnX, infoStartY)
368
+ .text("finn du också:", rightColumnX, infoStartY + 20);
369
+ doc
370
+ .fillColor(darkGray)
371
+ .fontSize(14)
372
+ .font(poppinsRegular)
373
+ .text("Information about bus departures", rightColumnX, infoStartY + 40)
374
+ .text("can also be found:", rightColumnX, infoStartY + 60);
375
+ doc
376
+ .fillColor(darkGray)
377
+ .fontSize(14)
378
+ .font(poppinsRegular)
379
+ .text("• I appane / in the apps:", rightColumnX, infoStartY + 100);
380
+ doc
381
+ .font(poppinsBold)
382
+ .text("FRAM / Entur", rightColumnX + 10, infoStartY + 120);
383
+ doc
384
+ .font(poppinsRegular)
385
+ .text("• På nettsidene / on the websites:", rightColumnX, infoStartY + 155);
386
+ doc
387
+ .font(poppinsBold)
388
+ .text("frammr.no / entur.no", rightColumnX + 10, infoStartY + 175);
389
+ }
390
+ else {
391
+ const qrSize = 120;
392
+ const qrX = iconX - iconSize / 2;
393
+ const qrY = contentY;
394
+ if (qrCodeBuffer) {
395
+ doc.image(qrCodeBuffer, qrX, qrY, { width: qrSize, height: qrSize });
467
396
  }
468
- doc.end();
469
- stream.on("finish", () => {
470
- resolvePromise();
471
- });
472
- stream.on("error", (error) => {
473
- reject(new Error(`Failed to write PDF: ${error.message}`));
397
+ const textStartX = qrX + qrSize + 20;
398
+ const rightColumnX = pageWidth - 320;
399
+ doc
400
+ .fillColor(darkGray)
401
+ .fontSize(24)
402
+ .font(poppinsBold)
403
+ .text("Når kjem bussen?", textStartX, contentY);
404
+ doc
405
+ .fillColor(darkGray)
406
+ .fontSize(14)
407
+ .font(poppinsRegular)
408
+ .text("Opne mobilkameraet ditt og hald", textStartX, contentY + 40)
409
+ .text("kameralinsa over QR-koden. Lenka", textStartX, contentY + 60)
410
+ .text("fører deg til reiseplanleggaren, og viser", textStartX, contentY + 80)
411
+ .text("busslinjer og avgangar frå haldeplassen", textStartX, contentY + 100)
412
+ .text("du står på.", textStartX, contentY + 120);
413
+ doc
414
+ .fillColor(darkGray)
415
+ .fontSize(24)
416
+ .font(poppinsBold)
417
+ .text("When will the bus", textStartX, contentY + 160)
418
+ .text("arrive?", textStartX, contentY + 185);
419
+ doc
420
+ .fillColor(darkGray)
421
+ .fontSize(14)
422
+ .font(poppinsRegular)
423
+ .text("Open your mobile camera and hold", textStartX, contentY + 220)
424
+ .text("camera lens over the QR code. The link", textStartX, contentY + 240)
425
+ .text("takes you to the travel planner, and", textStartX, contentY + 260)
426
+ .text("shows bus lines and departures from the", textStartX, contentY + 280)
427
+ .text("stop you are at.", textStartX, contentY + 300);
428
+ const separatorX = rightColumnX - 22;
429
+ doc
430
+ .lineWidth(0.25)
431
+ .moveTo(separatorX, contentY)
432
+ .lineTo(separatorX, contentY + 320)
433
+ .stroke("#000000");
434
+ const rightColumnStartY = contentY + 40;
435
+ doc
436
+ .fillColor(darkGray)
437
+ .fontSize(14)
438
+ .font(poppinsBold)
439
+ .text("Informasjon om bussavgangar", rightColumnX, rightColumnStartY)
440
+ .text("finn du også:", rightColumnX, rightColumnStartY + 20);
441
+ doc
442
+ .fillColor(darkGray)
443
+ .fontSize(14)
444
+ .font(poppinsRegular)
445
+ .text("Information about bus departures", rightColumnX, rightColumnStartY + 40)
446
+ .text("can also be found:", rightColumnX, rightColumnStartY + 60);
447
+ doc
448
+ .fillColor(darkGray)
449
+ .fontSize(14)
450
+ .font(poppinsRegular)
451
+ .text("• I appane / in the apps:", rightColumnX, rightColumnStartY + 100);
452
+ doc
453
+ .font(poppinsBold)
454
+ .text("FRAM / Entur", rightColumnX + 10, rightColumnStartY + 120);
455
+ doc
456
+ .font(poppinsRegular)
457
+ .text("• På nettsidene / on the websites:", rightColumnX, rightColumnStartY + 155);
458
+ doc
459
+ .font(poppinsBold)
460
+ .text("frammr.no / entur.no", rightColumnX + 10, rightColumnStartY + 175);
461
+ }
462
+ const footerY = pageHeight - footerHeight;
463
+ doc
464
+ .save()
465
+ .roundedRect(borderMargin, borderMargin, boxWidth, boxHeight, borderRadius)
466
+ .clip()
467
+ .rect(borderMargin, footerY, boxWidth, footerHeight)
468
+ .fill(headerFooterColor)
469
+ .restore();
470
+ try {
471
+ const logoBuffer = await node_fs_1.promises.readFile(logoPath);
472
+ const logoHeight = Math.round(logoWidth * 0.27);
473
+ const logoX = pageWidth - borderMargin - logoWidth - 20;
474
+ const logoY = footerY + footerHeight - logoHeight - 35;
475
+ doc.image(logoBuffer, logoX, logoY, {
476
+ width: logoWidth,
477
+ height: logoHeight,
474
478
  });
475
479
  }
476
480
  catch (error) {
477
- reject(new Error(`PDF generation failed: ${error instanceof Error ? error.message : "Unknown error"}`));
481
+ console.warn("Could not load logo image:", error);
482
+ doc
483
+ .fillColor(white)
484
+ .fontSize(24)
485
+ .font(poppinsBold)
486
+ .text(fallbackLogoText, pageWidth - 150, footerY + 25);
487
+ if (fallbackLogoSubtext) {
488
+ doc
489
+ .fillColor(white)
490
+ .fontSize(10)
491
+ .font(poppinsRegular)
492
+ .text(fallbackLogoSubtext, pageWidth - 220, footerY + 55);
493
+ }
478
494
  }
495
+ doc.end();
496
+ }
497
+ catch (error) {
498
+ throw new Error(`PDF generation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
499
+ }
500
+ await new Promise((resolve, reject) => {
501
+ stream.on("finish", () => {
502
+ resolve();
503
+ });
504
+ stream.on("error", (error) => {
505
+ reject(new Error(`Failed to write PDF: ${error.message}`));
506
+ });
479
507
  });
480
508
  }
481
509
  function slugify(text) {
@@ -498,7 +526,7 @@ function slugify(text) {
498
526
  }
499
527
  function generateSafeFilename(id, stopPlaceName) {
500
528
  const safeId = id.replace(/[^a-zA-Z0-9\-_]/g, "_");
501
- if (stopPlaceName && stopPlaceName.trim()) {
529
+ if (stopPlaceName?.trim()) {
502
530
  const slugifiedName = slugify(stopPlaceName);
503
531
  if (slugifiedName) {
504
532
  return `${safeId}-${slugifiedName}`;
@@ -508,15 +536,15 @@ function generateSafeFilename(id, stopPlaceName) {
508
536
  }
509
537
  async function ensureDirectoryExists(dirPath) {
510
538
  try {
511
- await fs_1.promises.access(dirPath);
539
+ await node_fs_1.promises.access(dirPath);
512
540
  }
513
541
  catch {
514
- await fs_1.promises.mkdir(dirPath, { recursive: true });
542
+ await node_fs_1.promises.mkdir(dirPath, { recursive: true });
515
543
  }
516
544
  }
517
545
  async function pathExists(path) {
518
546
  try {
519
- await fs_1.promises.access(path);
547
+ await node_fs_1.promises.access(path);
520
548
  return true;
521
549
  }
522
550
  catch {