@juspay/neurolink 9.95.0 → 9.95.2
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/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +232 -232
- package/dist/cli/factories/commandFactory.d.ts +10 -0
- package/dist/cli/factories/commandFactory.js +118 -13
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/utils/inputValidation.d.ts +22 -9
- package/dist/cli/utils/inputValidation.js +147 -61
- package/dist/core/baseProvider.js +1 -0
- package/dist/core/constants.d.ts +2 -0
- package/dist/core/constants.js +9 -0
- package/dist/core/modules/MessageBuilder.js +2 -0
- package/dist/lib/core/baseProvider.js +1 -0
- package/dist/lib/core/constants.d.ts +2 -0
- package/dist/lib/core/constants.js +9 -0
- package/dist/lib/core/modules/MessageBuilder.js +2 -0
- package/dist/lib/neurolink.js +8 -4
- package/dist/lib/types/cli.d.ts +7 -2
- package/dist/lib/types/file.d.ts +10 -0
- package/dist/lib/types/generate.d.ts +14 -0
- package/dist/lib/types/stream.d.ts +7 -0
- package/dist/lib/utils/errorHandling.d.ts +10 -0
- package/dist/lib/utils/errorHandling.js +28 -0
- package/dist/lib/utils/logger.d.ts +12 -0
- package/dist/lib/utils/logger.js +16 -0
- package/dist/lib/utils/messageBuilder.js +36 -5
- package/dist/lib/utils/multimodalOptionsBuilder.d.ts +4 -0
- package/dist/lib/utils/multimodalOptionsBuilder.js +1 -0
- package/dist/lib/utils/pdfProcessor.d.ts +15 -0
- package/dist/lib/utils/pdfProcessor.js +83 -3
- package/dist/neurolink.js +8 -4
- package/dist/types/cli.d.ts +7 -2
- package/dist/types/file.d.ts +10 -0
- package/dist/types/generate.d.ts +14 -0
- package/dist/types/stream.d.ts +7 -0
- package/dist/utils/errorHandling.d.ts +10 -0
- package/dist/utils/errorHandling.js +28 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +16 -0
- package/dist/utils/messageBuilder.js +36 -5
- package/dist/utils/multimodalOptionsBuilder.d.ts +4 -0
- package/dist/utils/multimodalOptionsBuilder.js +1 -0
- package/dist/utils/pdfProcessor.d.ts +15 -0
- package/dist/utils/pdfProcessor.js +83 -3
- package/package.json +1 -1
|
@@ -52,6 +52,8 @@ export const ERROR_CODES = {
|
|
|
52
52
|
CSV_PROCESSING_FAILED: "CSV_PROCESSING_FAILED",
|
|
53
53
|
// PDF validation errors
|
|
54
54
|
PDF_PAGE_LIMIT_EXCEEDED: "PDF_PAGE_LIMIT_EXCEEDED",
|
|
55
|
+
PDF_PASSWORD_REQUIRED: "PDF_PASSWORD_REQUIRED",
|
|
56
|
+
PDF_INCORRECT_PASSWORD: "PDF_INCORRECT_PASSWORD",
|
|
55
57
|
// Rate limiter errors
|
|
56
58
|
RATE_LIMITER_QUEUE_FULL: "RATE_LIMITER_QUEUE_FULL",
|
|
57
59
|
RATE_LIMITER_QUEUE_TIMEOUT: "RATE_LIMITER_QUEUE_TIMEOUT",
|
|
@@ -553,6 +555,32 @@ export class ErrorFactory {
|
|
|
553
555
|
},
|
|
554
556
|
});
|
|
555
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* The PDF is encrypted and no password was supplied (#258).
|
|
560
|
+
*/
|
|
561
|
+
static pdfPasswordRequired() {
|
|
562
|
+
return new NeuroLinkError({
|
|
563
|
+
code: ERROR_CODES.PDF_PASSWORD_REQUIRED,
|
|
564
|
+
message: "This PDF is password-protected. Supply the password via " +
|
|
565
|
+
"`pdfOptions: { password: '…' }` (SDK) or `--pdf-password` (CLI).",
|
|
566
|
+
category: ErrorCategory.VALIDATION,
|
|
567
|
+
severity: ErrorSeverity.MEDIUM,
|
|
568
|
+
retriable: false,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* A password was supplied for an encrypted PDF but it was incorrect (#258).
|
|
573
|
+
*/
|
|
574
|
+
static pdfIncorrectPassword() {
|
|
575
|
+
return new NeuroLinkError({
|
|
576
|
+
code: ERROR_CODES.PDF_INCORRECT_PASSWORD,
|
|
577
|
+
message: "The password supplied for this PDF is incorrect. Check the " +
|
|
578
|
+
"`pdfOptions.password` / `--pdf-password` value and try again.",
|
|
579
|
+
category: ErrorCategory.VALIDATION,
|
|
580
|
+
severity: ErrorSeverity.MEDIUM,
|
|
581
|
+
retriable: false,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
556
584
|
/**
|
|
557
585
|
* Create an image too large error
|
|
558
586
|
*/
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -162,6 +162,17 @@ declare class NeuroLinkLogger {
|
|
|
162
162
|
* @param args - The arguments to log. These are passed directly to `console.log`.
|
|
163
163
|
*/
|
|
164
164
|
always(...args: unknown[]): void;
|
|
165
|
+
/**
|
|
166
|
+
* Logs messages unconditionally using `console.error` (stderr).
|
|
167
|
+
*
|
|
168
|
+
* Same semantics as `always()` — bypasses log level checks and debug mode
|
|
169
|
+
* gating — but targets stderr instead of stdout. Use this for output that
|
|
170
|
+
* must stay visible (safety warnings, notices) without risking corruption
|
|
171
|
+
* of machine-readable stdout (e.g. `--format json`).
|
|
172
|
+
*
|
|
173
|
+
* @param args - The arguments to log. These are passed directly to `console.error`.
|
|
174
|
+
*/
|
|
175
|
+
alwaysStderr(...args: unknown[]): void;
|
|
165
176
|
/**
|
|
166
177
|
* Displays tabular data unconditionally using `console.table`.
|
|
167
178
|
*
|
|
@@ -200,6 +211,7 @@ export declare const logger: {
|
|
|
200
211
|
warn: (...args: unknown[]) => void;
|
|
201
212
|
error: (...args: unknown[]) => void;
|
|
202
213
|
always: (...args: unknown[]) => void;
|
|
214
|
+
alwaysStderr: (...args: unknown[]) => void;
|
|
203
215
|
table: (data: unknown) => void;
|
|
204
216
|
shouldLog: (level: LogLevel) => boolean;
|
|
205
217
|
setLogLevel: (level: LogLevel) => void;
|
package/dist/utils/logger.js
CHANGED
|
@@ -347,6 +347,19 @@ class NeuroLinkLogger {
|
|
|
347
347
|
always(...args) {
|
|
348
348
|
console.log(...args);
|
|
349
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Logs messages unconditionally using `console.error` (stderr).
|
|
352
|
+
*
|
|
353
|
+
* Same semantics as `always()` — bypasses log level checks and debug mode
|
|
354
|
+
* gating — but targets stderr instead of stdout. Use this for output that
|
|
355
|
+
* must stay visible (safety warnings, notices) without risking corruption
|
|
356
|
+
* of machine-readable stdout (e.g. `--format json`).
|
|
357
|
+
*
|
|
358
|
+
* @param args - The arguments to log. These are passed directly to `console.error`.
|
|
359
|
+
*/
|
|
360
|
+
alwaysStderr(...args) {
|
|
361
|
+
console.error(...args);
|
|
362
|
+
}
|
|
350
363
|
/**
|
|
351
364
|
* Displays tabular data unconditionally using `console.table`.
|
|
352
365
|
*
|
|
@@ -436,6 +449,9 @@ export const logger = {
|
|
|
436
449
|
always: (...args) => {
|
|
437
450
|
neuroLinkLogger.always(...args);
|
|
438
451
|
},
|
|
452
|
+
alwaysStderr: (...args) => {
|
|
453
|
+
neuroLinkLogger.alwaysStderr(...args);
|
|
454
|
+
},
|
|
439
455
|
table: (data) => {
|
|
440
456
|
neuroLinkLogger.table(data);
|
|
441
457
|
},
|
|
@@ -1011,6 +1011,12 @@ async function processExplicitPdfFiles(options, maxSize, provider) {
|
|
|
1011
1011
|
buffer: result.content,
|
|
1012
1012
|
filename,
|
|
1013
1013
|
pageCount: result.metadata?.estimatedPages ?? null,
|
|
1014
|
+
// #258: carry the password so the image-fallback conversion can
|
|
1015
|
+
// decrypt an encrypted PDF for providers without native PDF support.
|
|
1016
|
+
password: options.pdfOptions?.password,
|
|
1017
|
+
// #260: carry the per-page canvas-pixel ceiling so the caller can
|
|
1018
|
+
// raise (or lower) the memory guard for the image-fallback render.
|
|
1019
|
+
maxCanvasPixels: options.pdfOptions?.maxCanvasPixels,
|
|
1014
1020
|
});
|
|
1015
1021
|
logger.info(`[PDF] ✅ Queued for multimodal: ${filename} (${result.metadata?.estimatedPages ?? "unknown"} pages)`);
|
|
1016
1022
|
}
|
|
@@ -1103,7 +1109,13 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
|
|
|
1103
1109
|
// Check if this is a multimodal request
|
|
1104
1110
|
const hasImages = (inp.images && inp.images.length > 0) ||
|
|
1105
1111
|
(inp.content && inp.content.some((c) => c.type === "image"));
|
|
1106
|
-
|
|
1112
|
+
// A PDF supplied only via input.content (type: "pdf", no explicit
|
|
1113
|
+
// input.pdfFiles and no image alongside it) must still route through the
|
|
1114
|
+
// multimodal path below — otherwise it silently falls through to the
|
|
1115
|
+
// text-only branch and the PDF (and any pdfOptions) never reaches
|
|
1116
|
+
// convertContentToProviderFormat at all.
|
|
1117
|
+
const hasPDFs = pdfFiles.length > 0 ||
|
|
1118
|
+
!!(inp.content && inp.content.some((c) => c.type === "pdf"));
|
|
1107
1119
|
// If no images or PDFs, use standard message building and convert to MultimodalChatMessage[]
|
|
1108
1120
|
if (!hasImages && !hasPDFs) {
|
|
1109
1121
|
// #289: CSV content[] items don't need vision, so they never reach the
|
|
@@ -1139,8 +1151,11 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
|
|
|
1139
1151
|
`Supported providers: ${ProviderImageAdapter.getVisionProviders().join(", ")}`);
|
|
1140
1152
|
}
|
|
1141
1153
|
const messages = [];
|
|
1142
|
-
// Build enhanced system prompt
|
|
1143
|
-
|
|
1154
|
+
// Build enhanced system prompt. Gate on the same `hasPDFs` predicate used
|
|
1155
|
+
// for routing above — a PDF supplied only via input.content (no explicit
|
|
1156
|
+
// input.pdfFiles) must still get the "treat inlined content as an
|
|
1157
|
+
// attachment" instruction, or the model can claim no files were attached.
|
|
1158
|
+
const systemPrompt = buildMultimodalSystemPrompt(options, hasPDFs);
|
|
1144
1159
|
if (systemPrompt.trim()) {
|
|
1145
1160
|
messages.push({
|
|
1146
1161
|
role: "system",
|
|
@@ -1206,7 +1221,7 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
|
|
|
1206
1221
|
try {
|
|
1207
1222
|
let userContent;
|
|
1208
1223
|
if (inp.content && inp.content.length > 0) {
|
|
1209
|
-
userContent = await convertContentToProviderFormat(inp.content, provider, model);
|
|
1224
|
+
userContent = await convertContentToProviderFormat(inp.content, provider, model, options.pdfOptions);
|
|
1210
1225
|
}
|
|
1211
1226
|
else if ((inp.images && inp.images.length > 0) || pdfFiles.length > 0) {
|
|
1212
1227
|
userContent = await convertMultimodalToProviderFormat(inp.text ?? "", inp.images || [], pdfFiles, provider, model);
|
|
@@ -1285,7 +1300,7 @@ async function appendCsvContentToText(csvItems, baseText) {
|
|
|
1285
1300
|
/**
|
|
1286
1301
|
* Convert advanced content format to provider-specific format
|
|
1287
1302
|
*/
|
|
1288
|
-
async function convertContentToProviderFormat(content, provider, _model) {
|
|
1303
|
+
async function convertContentToProviderFormat(content, provider, _model, pdfOptions) {
|
|
1289
1304
|
const textContent = content.find((c) => c.type === "text");
|
|
1290
1305
|
const imageContent = content.filter((c) => c.type === "image");
|
|
1291
1306
|
const pdfContent = content.filter((c) => c.type === "pdf");
|
|
@@ -1312,6 +1327,11 @@ async function convertContentToProviderFormat(content, provider, _model) {
|
|
|
1312
1327
|
buffer: typeof pdf.data === "string" ? Buffer.from(pdf.data, "base64") : pdf.data,
|
|
1313
1328
|
filename: pdf.metadata?.filename || "document.pdf",
|
|
1314
1329
|
pageCount: pdf.metadata?.pages ?? null,
|
|
1330
|
+
// #258/#260: carry password + canvas-pixel ceiling so a PDF supplied via
|
|
1331
|
+
// the advanced `input.content` array gets the same decryption/memory
|
|
1332
|
+
// guard as the `input.pdfFiles` path (see `processExplicitPdfFiles`).
|
|
1333
|
+
password: pdfOptions?.password,
|
|
1334
|
+
maxCanvasPixels: pdfOptions?.maxCanvasPixels,
|
|
1315
1335
|
}));
|
|
1316
1336
|
return await convertMultimodalToProviderFormat(text, images, pdfFiles, provider, _model);
|
|
1317
1337
|
}
|
|
@@ -1694,6 +1714,10 @@ async function convertMultimodalToProviderFormat(text, images, pdfFiles, provide
|
|
|
1694
1714
|
const conversionResult = await PDFImageConverter.convertToImages(pdf.buffer, {
|
|
1695
1715
|
scale: 2.0, // High quality for OCR/analysis
|
|
1696
1716
|
maxPages: 20, // Limit pages to prevent token overflow
|
|
1717
|
+
...(pdf.password ? { password: pdf.password } : {}), // #258
|
|
1718
|
+
...(pdf.maxCanvasPixels
|
|
1719
|
+
? { maxCanvasPixels: pdf.maxCanvasPixels }
|
|
1720
|
+
: {}), // #260
|
|
1697
1721
|
});
|
|
1698
1722
|
logger.info(`[PDF→Image] ✅ Converted ${pdf.filename}: ${conversionResult.pageCount} page(s) → images`);
|
|
1699
1723
|
// Add each page as an ImagePart (raw base64, not data: URI — see SSRF note above)
|
|
@@ -1715,6 +1739,13 @@ async function convertMultimodalToProviderFormat(text, images, pdfFiles, provide
|
|
|
1715
1739
|
catch (error) {
|
|
1716
1740
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1717
1741
|
logger.error(`[PDF→Image] ❌ Failed to convert ${pdf.filename}: ${errorMessage}`);
|
|
1742
|
+
// #258: password errors are already actionable typed errors — re-throw
|
|
1743
|
+
// them unwrapped so the "supply a password" guidance isn't buried.
|
|
1744
|
+
const code = error?.code;
|
|
1745
|
+
if (code === "PDF_PASSWORD_REQUIRED" ||
|
|
1746
|
+
code === "PDF_INCORRECT_PASSWORD") {
|
|
1747
|
+
throw error;
|
|
1748
|
+
}
|
|
1718
1749
|
// Re-throw so the user knows PDF processing failed
|
|
1719
1750
|
throw new Error(`PDF to image conversion failed for ${pdf.filename}: ${errorMessage}. ` +
|
|
1720
1751
|
`Provider ${provider} doesn't support native PDFs and image conversion failed.`, { cause: error });
|
|
@@ -51,6 +51,10 @@ export declare function buildMultimodalOptions(options: StreamOptions, providerN
|
|
|
51
51
|
pdfFiles: (string | Buffer<ArrayBufferLike>)[] | undefined;
|
|
52
52
|
};
|
|
53
53
|
csvOptions: import("../index.js").CSVProcessorOptions | undefined;
|
|
54
|
+
pdfOptions: {
|
|
55
|
+
password?: string;
|
|
56
|
+
maxCanvasPixels?: number;
|
|
57
|
+
} | undefined;
|
|
54
58
|
systemPrompt: string | undefined;
|
|
55
59
|
conversationHistory: import("../index.js").ChatMessage[] | undefined;
|
|
56
60
|
provider: string;
|
|
@@ -51,6 +51,7 @@ export function buildMultimodalOptions(options, providerName, modelName) {
|
|
|
51
51
|
pdfFiles: options.input?.pdfFiles,
|
|
52
52
|
},
|
|
53
53
|
csvOptions: options.csvOptions,
|
|
54
|
+
pdfOptions: options.pdfOptions,
|
|
54
55
|
systemPrompt: options.systemPrompt,
|
|
55
56
|
conversationHistory: options.conversationMessages,
|
|
56
57
|
provider: providerName,
|
|
@@ -21,6 +21,21 @@ export declare class PDFProcessor {
|
|
|
21
21
|
private static isValidPDF;
|
|
22
22
|
private static extractBasicMetadata;
|
|
23
23
|
static estimateTokens(pageCount: number, mode?: "text-only" | "visual"): number;
|
|
24
|
+
/**
|
|
25
|
+
* Estimate the largest page's rendered pixel count from the PDF's MediaBox
|
|
26
|
+
* entries, WITHOUT rendering (#260). Parses `/MediaBox [llx lly urx ury]`
|
|
27
|
+
* (dimensions in points); rendered pixels ≈ (width·scale)·(height·scale).
|
|
28
|
+
* Returns 0 when no plaintext MediaBox is found (e.g. compressed object
|
|
29
|
+
* streams) — the caller then skips downscaling, matching prior behavior.
|
|
30
|
+
*
|
|
31
|
+
* A crafted/malformed MediaBox can have finite but astronomically large
|
|
32
|
+
* width/height; `width * height * scale * scale` can then overflow past
|
|
33
|
+
* `Number.MAX_VALUE` to `Infinity`. Clamp the product to
|
|
34
|
+
* `Number.MAX_SAFE_INTEGER` so it stays finite — a large-but-finite pixel
|
|
35
|
+
* estimate still triggers downscaling, whereas `Infinity` would collapse
|
|
36
|
+
* the computed downscale factor to 0 (see `convertToImages`).
|
|
37
|
+
*/
|
|
38
|
+
private static largestPagePixels;
|
|
24
39
|
/**
|
|
25
40
|
* Convert a PDF buffer to an array of base64 PNG images
|
|
26
41
|
*
|
|
@@ -231,6 +231,40 @@ export class PDFProcessor {
|
|
|
231
231
|
// ============================================================================
|
|
232
232
|
// PDF → Image Conversion (for providers without native PDF support)
|
|
233
233
|
// ============================================================================
|
|
234
|
+
/**
|
|
235
|
+
* Estimate the largest page's rendered pixel count from the PDF's MediaBox
|
|
236
|
+
* entries, WITHOUT rendering (#260). Parses `/MediaBox [llx lly urx ury]`
|
|
237
|
+
* (dimensions in points); rendered pixels ≈ (width·scale)·(height·scale).
|
|
238
|
+
* Returns 0 when no plaintext MediaBox is found (e.g. compressed object
|
|
239
|
+
* streams) — the caller then skips downscaling, matching prior behavior.
|
|
240
|
+
*
|
|
241
|
+
* A crafted/malformed MediaBox can have finite but astronomically large
|
|
242
|
+
* width/height; `width * height * scale * scale` can then overflow past
|
|
243
|
+
* `Number.MAX_VALUE` to `Infinity`. Clamp the product to
|
|
244
|
+
* `Number.MAX_SAFE_INTEGER` so it stays finite — a large-but-finite pixel
|
|
245
|
+
* estimate still triggers downscaling, whereas `Infinity` would collapse
|
|
246
|
+
* the computed downscale factor to 0 (see `convertToImages`).
|
|
247
|
+
*/
|
|
248
|
+
static largestPagePixels(pdfBuffer, scale) {
|
|
249
|
+
const text = pdfBuffer.toString("latin1");
|
|
250
|
+
const re = /\/MediaBox\s*\[\s*(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s*\]/g;
|
|
251
|
+
let max = 0;
|
|
252
|
+
let match;
|
|
253
|
+
while ((match = re.exec(text)) !== null) {
|
|
254
|
+
const width = Math.abs(parseFloat(match[3]) - parseFloat(match[1]));
|
|
255
|
+
const height = Math.abs(parseFloat(match[4]) - parseFloat(match[2]));
|
|
256
|
+
if (Number.isFinite(width) && Number.isFinite(height)) {
|
|
257
|
+
const pixels = width * height * scale * scale;
|
|
258
|
+
const boundedPixels = Number.isFinite(pixels)
|
|
259
|
+
? Math.min(pixels, Number.MAX_SAFE_INTEGER)
|
|
260
|
+
: Number.MAX_SAFE_INTEGER;
|
|
261
|
+
if (boundedPixels > max) {
|
|
262
|
+
max = boundedPixels;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return max;
|
|
267
|
+
}
|
|
234
268
|
/**
|
|
235
269
|
* Convert a PDF buffer to an array of base64 PNG images
|
|
236
270
|
*
|
|
@@ -257,7 +291,7 @@ export class PDFProcessor {
|
|
|
257
291
|
*/
|
|
258
292
|
static async convertToImages(pdfBuffer, options) {
|
|
259
293
|
const startTime = Date.now();
|
|
260
|
-
const { scale = 2, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES, format = "png", } = options || {};
|
|
294
|
+
const { scale = 2, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES, format = "png", maxCanvasPixels = PDF_LIMITS.DEFAULT_MAX_CANVAS_PIXELS, password, } = options || {};
|
|
261
295
|
const images = [];
|
|
262
296
|
const warnings = [];
|
|
263
297
|
// ============================================================================
|
|
@@ -273,6 +307,11 @@ export class PDFProcessor {
|
|
|
273
307
|
if (!Number.isFinite(scale) || scale <= 0 || scale > PDF_LIMITS.MAX_SCALE) {
|
|
274
308
|
throw new Error(`Invalid scale: ${scale}. Scale must be a finite number greater than 0 and at most ${PDF_LIMITS.MAX_SCALE}.`);
|
|
275
309
|
}
|
|
310
|
+
// 0c. Validate the per-page pixel ceiling (#260). A non-finite or
|
|
311
|
+
// non-positive value would disable the guard or yield a NaN downscale.
|
|
312
|
+
if (!Number.isFinite(maxCanvasPixels) || maxCanvasPixels <= 0) {
|
|
313
|
+
throw new Error(`Invalid maxCanvasPixels: ${maxCanvasPixels}. Must be a finite number greater than 0.`);
|
|
314
|
+
}
|
|
276
315
|
// 1. Validate buffer is not empty or too small
|
|
277
316
|
if (!pdfBuffer || pdfBuffer.length < 5) {
|
|
278
317
|
throw new Error("Invalid PDF: Buffer is too small or empty. " +
|
|
@@ -303,8 +342,38 @@ export class PDFProcessor {
|
|
|
303
342
|
scale,
|
|
304
343
|
maxPages: maxPages || "all",
|
|
305
344
|
});
|
|
306
|
-
//
|
|
307
|
-
|
|
345
|
+
// #260: pre-flight page-size check WITHOUT rendering. pdf-to-img applies
|
|
346
|
+
// `scale` uniformly with no per-page hook and no pixel guard, so a very
|
|
347
|
+
// large page (e.g. an architectural drawing with a huge MediaBox) can
|
|
348
|
+
// allocate gigabytes of canvas. Read the largest MediaBox from the PDF
|
|
349
|
+
// bytes and, if that page at the requested scale would exceed
|
|
350
|
+
// maxCanvasPixels, downscale the whole render uniformly to stay under it.
|
|
351
|
+
let effectiveScale = scale;
|
|
352
|
+
const largestPixels = PDFProcessor.largestPagePixels(pdfBuffer, scale);
|
|
353
|
+
if (largestPixels > maxCanvasPixels) {
|
|
354
|
+
const downscale = Math.sqrt(maxCanvasPixels / largestPixels);
|
|
355
|
+
// Floor the result: an astronomically large (but now finite, see
|
|
356
|
+
// `largestPagePixels`) pixel estimate would otherwise push `downscale`
|
|
357
|
+
// — and therefore `effectiveScale` — toward 0, handing `pdf-to-img` a
|
|
358
|
+
// degenerate viewport instead of a small-but-renderable page.
|
|
359
|
+
effectiveScale = Math.max(PDF_LIMITS.MIN_EFFECTIVE_SCALE, scale * downscale);
|
|
360
|
+
// Recompute the ratio actually applied (may differ from `downscale`
|
|
361
|
+
// when the floor above kicks in) so the logged estimate stays honest.
|
|
362
|
+
const actualDownscale = effectiveScale / scale;
|
|
363
|
+
const beforeMB = (largestPixels * 4) / (1024 * 1024);
|
|
364
|
+
const afterMB = (largestPixels * actualDownscale * actualDownscale * 4) /
|
|
365
|
+
(1024 * 1024);
|
|
366
|
+
const msg = `Downscaled render (scale ${scale} → ${effectiveScale.toFixed(3)}): ` +
|
|
367
|
+
`the largest page would allocate ~${beforeMB.toFixed(0)}MB, above the ` +
|
|
368
|
+
`maxCanvasPixels ceiling; reduced to ~${afterMB.toFixed(0)}MB per page.`;
|
|
369
|
+
logger.warn(`[PDF→Image] ⚠️ ${msg}`);
|
|
370
|
+
warnings.push(msg);
|
|
371
|
+
}
|
|
372
|
+
// Create PDF document iterator (password forwarded for encrypted PDFs, #258)
|
|
373
|
+
const document = await pdf(pdfBuffer, {
|
|
374
|
+
scale: effectiveScale,
|
|
375
|
+
...(password ? { password } : {}),
|
|
376
|
+
});
|
|
308
377
|
let pageIndex = 0;
|
|
309
378
|
// Iterate through pages and convert to base64
|
|
310
379
|
for await (const page of document) {
|
|
@@ -346,6 +415,17 @@ export class PDFProcessor {
|
|
|
346
415
|
error: errorMessage,
|
|
347
416
|
conversionTimeMs,
|
|
348
417
|
});
|
|
418
|
+
// #258: map pdfjs's PasswordException to an actionable typed error so a
|
|
419
|
+
// caller learns to supply (or correct) the password instead of seeing a
|
|
420
|
+
// generic "conversion failed". pdfjs code 1 = NEED_PASSWORD, 2 = INCORRECT.
|
|
421
|
+
const pdfErr = error;
|
|
422
|
+
if (pdfErr?.name === "PasswordException" ||
|
|
423
|
+
/password/i.test(errorMessage)) {
|
|
424
|
+
const incorrect = pdfErr.code === 2 || /incorrect|invalid/i.test(errorMessage);
|
|
425
|
+
throw incorrect
|
|
426
|
+
? ErrorFactory.pdfIncorrectPassword()
|
|
427
|
+
: ErrorFactory.pdfPasswordRequired();
|
|
428
|
+
}
|
|
349
429
|
throw new Error(`PDF to image conversion failed: ${errorMessage}`, {
|
|
350
430
|
cause: error,
|
|
351
431
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.95.
|
|
3
|
+
"version": "9.95.2",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|