@juspay/neurolink 9.95.2 → 9.95.3
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 +6 -0
- package/dist/browser/neurolink.min.js +352 -352
- package/dist/core/constants.d.ts +3 -0
- package/dist/core/constants.js +10 -0
- package/dist/lib/core/constants.d.ts +3 -0
- package/dist/lib/core/constants.js +10 -0
- package/dist/lib/types/file.d.ts +38 -1
- package/dist/lib/utils/fileDetector.js +35 -0
- package/dist/lib/utils/messageBuilder.js +17 -0
- package/dist/lib/utils/pdfProcessor.d.ts +24 -1
- package/dist/lib/utils/pdfProcessor.js +191 -52
- package/dist/types/file.d.ts +38 -1
- package/dist/utils/fileDetector.js +35 -0
- package/dist/utils/messageBuilder.js +17 -0
- package/dist/utils/pdfProcessor.d.ts +24 -1
- package/dist/utils/pdfProcessor.js +191 -52
- package/package.json +1 -1
|
@@ -1413,6 +1413,41 @@ export class FileDetector {
|
|
|
1413
1413
|
const timeout = options?.timeout || FileDetector.DEFAULT_NETWORK_TIMEOUT;
|
|
1414
1414
|
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
1415
1415
|
const retryDelay = options?.retryDelay ?? DEFAULT_RETRY_DELAY;
|
|
1416
|
+
// #317: pre-flight HEAD to reject an oversized file BEFORE downloading any
|
|
1417
|
+
// body. content-length is advisory (chunked responses omit it), so a
|
|
1418
|
+
// missing/invalid header — or a server that refuses HEAD — falls through to
|
|
1419
|
+
// the streaming byte guard below; only a genuine oversize rejection stops
|
|
1420
|
+
// the GET from ever running.
|
|
1421
|
+
//
|
|
1422
|
+
// #323: skip the pre-flight entirely when this exact URL was recently seen
|
|
1423
|
+
// (its Content-Type is still cached, whether from a prior loadFromURL GET
|
|
1424
|
+
// or a MimeTypeStrategy HEAD) — issuing a fresh HEAD here would defeat the
|
|
1425
|
+
// whole point of that cache. The streaming byte guard in the GET below
|
|
1426
|
+
// still enforces maxSize even without a pre-flight, so this doesn't remove
|
|
1427
|
+
// the oversize protection — it only skips the redundant round-trip for a
|
|
1428
|
+
// URL we've already been talking to within the last 60s.
|
|
1429
|
+
if (getCachedUrlContentType(url, Date.now()) === undefined) {
|
|
1430
|
+
try {
|
|
1431
|
+
const head = await request(url, {
|
|
1432
|
+
dispatcher: getGlobalDispatcher().compose(interceptors.redirect({ maxRedirections: 5 })),
|
|
1433
|
+
method: "HEAD",
|
|
1434
|
+
headersTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
|
|
1435
|
+
bodyTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
|
|
1436
|
+
});
|
|
1437
|
+
// Drain/close the (empty) HEAD body so the connection can be reused.
|
|
1438
|
+
await head.body.dump();
|
|
1439
|
+
const declaredLength = Number(head.headers["content-length"]);
|
|
1440
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxSize) {
|
|
1441
|
+
throw new Error(`File too large: ${formatFileSize(declaredLength)} (max: ${formatFileSize(maxSize)})`);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
catch (error) {
|
|
1445
|
+
if (error instanceof Error && /File too large/.test(error.message)) {
|
|
1446
|
+
throw error;
|
|
1447
|
+
}
|
|
1448
|
+
logger.debug(`[FileDetector] HEAD pre-flight skipped for ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1416
1451
|
return withRetry(async () => {
|
|
1417
1452
|
try {
|
|
1418
1453
|
const response = await request(url, {
|
|
@@ -1026,6 +1026,23 @@ async function processExplicitPdfFiles(options, maxSize, provider) {
|
|
|
1026
1026
|
throw error;
|
|
1027
1027
|
}
|
|
1028
1028
|
}
|
|
1029
|
+
// #309: enforce the provider's page/size ceilings across ALL PDFs, not just
|
|
1030
|
+
// per-file. N files each just under the single-file limit can still blow past
|
|
1031
|
+
// it in aggregate (e.g. three 40-page PDFs → 120 pages for a 100-page API).
|
|
1032
|
+
const aggregateConfig = PDFProcessor.getProviderConfig(provider);
|
|
1033
|
+
if (aggregateConfig && pdfFiles.length > 1) {
|
|
1034
|
+
const totalPages = pdfFiles.reduce((sum, f) => sum + (f.pageCount ?? 0), 0);
|
|
1035
|
+
const totalMB = pdfFiles.reduce((sum, f) => sum + f.buffer.length, 0) / (1024 * 1024);
|
|
1036
|
+
if (totalPages > aggregateConfig.maxPages) {
|
|
1037
|
+
throw new Error(`[PDF] Combined page count across ${pdfFiles.length} PDFs (${totalPages}) exceeds the ` +
|
|
1038
|
+
`${aggregateConfig.maxPages}-page limit for ${provider}. ` +
|
|
1039
|
+
`Split the request or reduce the number of PDFs.`);
|
|
1040
|
+
}
|
|
1041
|
+
if (totalMB > aggregateConfig.maxSizeMB) {
|
|
1042
|
+
throw new Error(`[PDF] Combined size across ${pdfFiles.length} PDFs (${totalMB.toFixed(2)}MB) exceeds the ` +
|
|
1043
|
+
`${aggregateConfig.maxSizeMB}MB limit for ${provider}.`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1029
1046
|
return pdfFiles;
|
|
1030
1047
|
}
|
|
1031
1048
|
/**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The conversion uses pdf-to-img package (MuPDF-based) for high-quality conversion.
|
|
9
9
|
*/
|
|
10
|
-
import type { FileProcessingResult, PDFProcessorOptions, PDFProviderConfig, PDFImageConversionOptions, PDFImageConversionResult } from "../types/index.js";
|
|
10
|
+
import type { FileProcessingResult, PDFProcessorOptions, PDFProviderConfig, PDFImageConversionOptions, PDFImageConversionResult, PDFImagePage } from "../types/index.js";
|
|
11
11
|
export declare class PDFProcessor {
|
|
12
12
|
private static readonly PDF_SIGNATURE;
|
|
13
13
|
static process(content: Buffer, options?: PDFProcessorOptions): Promise<FileProcessingResult>;
|
|
@@ -20,6 +20,15 @@ export declare class PDFProcessor {
|
|
|
20
20
|
static getProviderConfig(provider: string): PDFProviderConfig | null;
|
|
21
21
|
private static isValidPDF;
|
|
22
22
|
private static extractBasicMetadata;
|
|
23
|
+
/**
|
|
24
|
+
* Accurate page count via pdf-parse (pdfjs) (#287). The header regex in
|
|
25
|
+
* `extractBasicMetadata` only sees plaintext `/Type /Page` markers and misses
|
|
26
|
+
* compressed/object-stream PDFs (and miscounts when a page dict spans a chunk
|
|
27
|
+
* boundary). This parses the document properly, bounded by a timeout so a
|
|
28
|
+
* pathological PDF can't block; returns null on timeout/failure so the caller
|
|
29
|
+
* falls back to the regex estimate.
|
|
30
|
+
*/
|
|
31
|
+
private static getAccuratePageCount;
|
|
23
32
|
static estimateTokens(pageCount: number, mode?: "text-only" | "visual"): number;
|
|
24
33
|
/**
|
|
25
34
|
* Estimate the largest page's rendered pixel count from the PDF's MediaBox
|
|
@@ -61,6 +70,20 @@ export declare class PDFProcessor {
|
|
|
61
70
|
* ```
|
|
62
71
|
*/
|
|
63
72
|
static convertToImages(pdfBuffer: Buffer, options?: PDFImageConversionOptions): Promise<PDFImageConversionResult>;
|
|
73
|
+
/**
|
|
74
|
+
* Shared input validation for the image-conversion paths. Returns the PDF
|
|
75
|
+
* size in MB (needed by the memory-estimate log). Throws on any invalid input
|
|
76
|
+
* with the same messages the batch path has always used.
|
|
77
|
+
*/
|
|
78
|
+
private static validateImageConversionInput;
|
|
79
|
+
/**
|
|
80
|
+
* Streaming variant of {@link convertToImages} (#302): yields each page's
|
|
81
|
+
* base64 PNG as soon as it renders instead of buffering the whole document,
|
|
82
|
+
* and reports progress via `options.onProgress`. A page that fails to render
|
|
83
|
+
* is yielded with `error` set (per-page isolation, #294) rather than aborting
|
|
84
|
+
* the stream. `convertToImages` is the batch wrapper over this contract.
|
|
85
|
+
*/
|
|
86
|
+
static convertToImagesStream(pdfBuffer: Buffer, options?: PDFImageConversionOptions): AsyncGenerator<PDFImagePage, void, void>;
|
|
64
87
|
/**
|
|
65
88
|
* Convert a PDF file path to an array of base64 PNG images
|
|
66
89
|
*
|
|
@@ -150,6 +150,13 @@ export class PDFProcessor {
|
|
|
150
150
|
throw new Error(`PDF size ${sizeMB.toFixed(2)}MB exceeds ${config.maxSizeMB}MB limit for ${provider}`);
|
|
151
151
|
}
|
|
152
152
|
const metadata = PDFProcessor.extractBasicMetadata(content);
|
|
153
|
+
// #287: prefer an accurate page count (pdf-parse/pdfjs) over the unreliable
|
|
154
|
+
// header regex for both limit enforcement and returned metadata; fall back
|
|
155
|
+
// to the regex estimate when the accurate probe times out or fails.
|
|
156
|
+
const accuratePages = await PDFProcessor.getAccuratePageCount(content);
|
|
157
|
+
if (accuratePages !== null) {
|
|
158
|
+
metadata.estimatedPages = accuratePages;
|
|
159
|
+
}
|
|
153
160
|
if (metadata.estimatedPages && metadata.estimatedPages > config.maxPages) {
|
|
154
161
|
const enforceLimits = options?.enforceLimits !== false;
|
|
155
162
|
if (enforceLimits) {
|
|
@@ -185,6 +192,10 @@ export class PDFProcessor {
|
|
|
185
192
|
...metadata,
|
|
186
193
|
provider,
|
|
187
194
|
apiType: config.apiType,
|
|
195
|
+
// #349: surface the provider's citations requirement so downstream
|
|
196
|
+
// provider adapters (e.g. Bedrock Converse document blocks) can act on
|
|
197
|
+
// it, instead of the config field being read nowhere.
|
|
198
|
+
requiresCitations: config.requiresCitations,
|
|
188
199
|
},
|
|
189
200
|
};
|
|
190
201
|
}
|
|
@@ -220,6 +231,35 @@ export class PDFProcessor {
|
|
|
220
231
|
filename: undefined,
|
|
221
232
|
};
|
|
222
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Accurate page count via pdf-parse (pdfjs) (#287). The header regex in
|
|
236
|
+
* `extractBasicMetadata` only sees plaintext `/Type /Page` markers and misses
|
|
237
|
+
* compressed/object-stream PDFs (and miscounts when a page dict spans a chunk
|
|
238
|
+
* boundary). This parses the document properly, bounded by a timeout so a
|
|
239
|
+
* pathological PDF can't block; returns null on timeout/failure so the caller
|
|
240
|
+
* falls back to the regex estimate.
|
|
241
|
+
*/
|
|
242
|
+
static async getAccuratePageCount(buffer) {
|
|
243
|
+
try {
|
|
244
|
+
const { PDFParse } = await import("pdf-parse");
|
|
245
|
+
const pdf = new PDFParse({ data: new Uint8Array(buffer) });
|
|
246
|
+
try {
|
|
247
|
+
const info = await Promise.race([
|
|
248
|
+
pdf.getInfo(),
|
|
249
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("page-count timeout")), PDF_LIMITS.PAGE_COUNT_TIMEOUT_MS)),
|
|
250
|
+
]);
|
|
251
|
+
const total = info.total;
|
|
252
|
+
return typeof total === "number" && total > 0 ? total : null;
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
await pdf.destroy?.();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
logger.debug(`[PDF] Accurate page count unavailable; using regex estimate: ${error instanceof Error ? error.message : String(error)}`);
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
223
263
|
static estimateTokens(pageCount, mode = "visual") {
|
|
224
264
|
if (mode === "text-only") {
|
|
225
265
|
return Math.ceil((pageCount / 3) * 1000);
|
|
@@ -291,48 +331,32 @@ export class PDFProcessor {
|
|
|
291
331
|
*/
|
|
292
332
|
static async convertToImages(pdfBuffer, options) {
|
|
293
333
|
const startTime = Date.now();
|
|
294
|
-
const { scale =
|
|
334
|
+
const { scale = PDF_LIMITS.DEFAULT_SCALE, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES, format = "png", maxCanvasPixels = PDF_LIMITS.DEFAULT_MAX_CANVAS_PIXELS, password, onProgress, } = options || {};
|
|
295
335
|
const images = [];
|
|
296
336
|
const warnings = [];
|
|
297
|
-
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
// 0b. Validate the render scale. A non-finite or non-positive scale yields a
|
|
305
|
-
// degenerate viewport (blank/zero-dimension render), and an excessive scale
|
|
306
|
-
// can allocate hundreds of MB per page — reject both with a clear message.
|
|
307
|
-
if (!Number.isFinite(scale) || scale <= 0 || scale > PDF_LIMITS.MAX_SCALE) {
|
|
308
|
-
throw new Error(`Invalid scale: ${scale}. Scale must be a finite number greater than 0 and at most ${PDF_LIMITS.MAX_SCALE}.`);
|
|
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
|
-
}
|
|
315
|
-
// 1. Validate buffer is not empty or too small
|
|
316
|
-
if (!pdfBuffer || pdfBuffer.length < 5) {
|
|
317
|
-
throw new Error("Invalid PDF: Buffer is too small or empty. " +
|
|
318
|
-
"A valid PDF must be at least 5 bytes (PDF header).");
|
|
319
|
-
}
|
|
320
|
-
// 2. Validate PDF magic bytes (%PDF-)
|
|
321
|
-
if (!PDFProcessor.isValidPDF(pdfBuffer)) {
|
|
322
|
-
throw new Error("Invalid PDF: File must start with %PDF- header. " +
|
|
323
|
-
"The provided buffer does not appear to be a valid PDF file.");
|
|
324
|
-
}
|
|
325
|
-
// 3. Validate maximum buffer size to prevent memory exhaustion
|
|
326
|
-
const sizeMB = pdfBuffer.length / (1024 * 1024);
|
|
327
|
-
if (sizeMB > PDF_LIMITS.MAX_SIZE_MB) {
|
|
328
|
-
throw new Error(`PDF too large for image conversion: ${sizeMB.toFixed(2)}MB exceeds ${PDF_LIMITS.MAX_SIZE_MB}MB limit. ` +
|
|
329
|
-
"Consider splitting the PDF or using a provider with native PDF support.");
|
|
330
|
-
}
|
|
337
|
+
const pageErrors = [];
|
|
338
|
+
// Validation shared with convertToImagesStream (#302).
|
|
339
|
+
const sizeMB = PDFProcessor.validateImageConversionInput(pdfBuffer, {
|
|
340
|
+
format,
|
|
341
|
+
scale,
|
|
342
|
+
maxCanvasPixels,
|
|
343
|
+
});
|
|
331
344
|
logger.debug("[PDF→Image] ✅ PDF validation passed", {
|
|
332
345
|
bufferSize: pdfBuffer.length,
|
|
333
346
|
sizeMB: sizeMB.toFixed(2),
|
|
334
347
|
maxPages,
|
|
335
348
|
});
|
|
349
|
+
// #297: surface an up-front memory estimate so an operator can spot a
|
|
350
|
+
// conversion that will allocate a lot before it runs. Uses the cheap regex
|
|
351
|
+
// page estimate (the accurate pdf-parse count runs in process(), not here).
|
|
352
|
+
const estimatedPages = Math.max(1, PDFProcessor.extractBasicMetadata(pdfBuffer).estimatedPages ?? 1);
|
|
353
|
+
const estimatedMemoryMB = PDFProcessor.estimateConversionMemoryUsage(pdfBuffer.length, estimatedPages, scale);
|
|
354
|
+
logger.info("[PDF→Image] Estimated memory usage before conversion", {
|
|
355
|
+
scale,
|
|
356
|
+
estimatedPages,
|
|
357
|
+
estimatedMemoryMB,
|
|
358
|
+
sizeMB: Number(sizeMB.toFixed(2)),
|
|
359
|
+
});
|
|
336
360
|
try {
|
|
337
361
|
// Dynamic import to avoid loading MuPDF binaries until needed
|
|
338
362
|
const pdfToImgModule = await import("pdf-to-img");
|
|
@@ -369,35 +393,60 @@ export class PDFProcessor {
|
|
|
369
393
|
logger.warn(`[PDF→Image] ⚠️ ${msg}`);
|
|
370
394
|
warnings.push(msg);
|
|
371
395
|
}
|
|
372
|
-
// Create PDF document
|
|
396
|
+
// Create PDF document (password forwarded for encrypted PDFs, #258).
|
|
397
|
+
// pdf-to-img resolves `.length` (numPages) synchronously here and exposes
|
|
398
|
+
// `.getPage(n)`, so we drive an indexed loop rather than the async
|
|
399
|
+
// iterator — that lets one bad page be isolated instead of aborting all.
|
|
373
400
|
const document = await pdf(pdfBuffer, {
|
|
374
401
|
scale: effectiveScale,
|
|
375
402
|
...(password ? { password } : {}),
|
|
376
403
|
});
|
|
377
|
-
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
404
|
+
const totalPages = document.length;
|
|
405
|
+
// #294: convert page-by-page with per-page isolation. A single page whose
|
|
406
|
+
// canvas render throws (e.g. a degenerate MediaBox) no longer discards
|
|
407
|
+
// every already-converted page — it is recorded in `errors` and the rest
|
|
408
|
+
// continue.
|
|
409
|
+
for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
|
|
410
|
+
if (maxPages !== undefined && pageNum - 1 >= maxPages) {
|
|
411
|
+
warnings.push(`Stopped at page ${pageNum - 1} (maxPages limit: ${maxPages})`);
|
|
383
412
|
break;
|
|
384
413
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
414
|
+
try {
|
|
415
|
+
const page = await document.getPage(pageNum);
|
|
416
|
+
const base64Image = page.toString("base64");
|
|
417
|
+
images.push(base64Image);
|
|
418
|
+
logger.debug(`[PDF→Image] Converted page ${pageNum}`, {
|
|
419
|
+
imageSizeBytes: page.length,
|
|
420
|
+
base64Length: base64Image.length,
|
|
421
|
+
});
|
|
422
|
+
// #302: report progress after each successfully converted page.
|
|
423
|
+
if (onProgress) {
|
|
424
|
+
await onProgress({
|
|
425
|
+
pagesConverted: images.length,
|
|
426
|
+
totalPages,
|
|
427
|
+
elapsedMs: Date.now() - startTime,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch (pageError) {
|
|
432
|
+
const msg = pageError instanceof Error ? pageError.message : String(pageError);
|
|
433
|
+
logger.warn(`[PDF→Image] ⚠️ page ${pageNum} failed to render: ${msg}`);
|
|
434
|
+
pageErrors.push({ page: pageNum, error: msg });
|
|
435
|
+
}
|
|
393
436
|
}
|
|
394
|
-
//
|
|
437
|
+
// Empty PDF (0 pages) or every page failed → treat as a conversion
|
|
438
|
+
// failure (kept inside the try so the password/format mapping below still
|
|
439
|
+
// applies), otherwise return the pages that did convert.
|
|
395
440
|
if (images.length === 0) {
|
|
441
|
+
if (pageErrors.length > 0) {
|
|
442
|
+
throw new Error(`All ${pageErrors.length} page(s) failed to render. First error: ${pageErrors[0].error}`);
|
|
443
|
+
}
|
|
396
444
|
throw new Error("PDF has 0 pages. Cannot convert empty PDF to images.");
|
|
397
445
|
}
|
|
398
446
|
const conversionTimeMs = Date.now() - startTime;
|
|
399
447
|
logger.info("[PDF→Image] ✅ PDF conversion completed", {
|
|
400
448
|
pageCount: images.length,
|
|
449
|
+
failedPages: pageErrors.length,
|
|
401
450
|
conversionTimeMs,
|
|
402
451
|
totalImageBytes: images.reduce((sum, img) => sum + img.length, 0),
|
|
403
452
|
});
|
|
@@ -406,6 +455,7 @@ export class PDFProcessor {
|
|
|
406
455
|
pageCount: images.length,
|
|
407
456
|
conversionTimeMs,
|
|
408
457
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
458
|
+
errors: pageErrors.length > 0 ? pageErrors : undefined,
|
|
409
459
|
};
|
|
410
460
|
}
|
|
411
461
|
catch (error) {
|
|
@@ -431,6 +481,95 @@ export class PDFProcessor {
|
|
|
431
481
|
});
|
|
432
482
|
}
|
|
433
483
|
}
|
|
484
|
+
/**
|
|
485
|
+
* Shared input validation for the image-conversion paths. Returns the PDF
|
|
486
|
+
* size in MB (needed by the memory-estimate log). Throws on any invalid input
|
|
487
|
+
* with the same messages the batch path has always used.
|
|
488
|
+
*/
|
|
489
|
+
static validateImageConversionInput(pdfBuffer, opts) {
|
|
490
|
+
if (opts.format !== "png") {
|
|
491
|
+
throw new Error(`Invalid format: "${opts.format}". Only "png" format is currently supported.`);
|
|
492
|
+
}
|
|
493
|
+
if (!Number.isFinite(opts.scale) ||
|
|
494
|
+
opts.scale < PDF_LIMITS.MIN_SCALE ||
|
|
495
|
+
opts.scale > PDF_LIMITS.MAX_SCALE) {
|
|
496
|
+
throw new Error(`Invalid scale: ${opts.scale}. Scale must be a finite number between ${PDF_LIMITS.MIN_SCALE} and ${PDF_LIMITS.MAX_SCALE}.`);
|
|
497
|
+
}
|
|
498
|
+
if (!Number.isFinite(opts.maxCanvasPixels) || opts.maxCanvasPixels <= 0) {
|
|
499
|
+
throw new Error(`Invalid maxCanvasPixels: ${opts.maxCanvasPixels}. Must be a finite number greater than 0.`);
|
|
500
|
+
}
|
|
501
|
+
if (!pdfBuffer || pdfBuffer.length < 5) {
|
|
502
|
+
throw new Error("Invalid PDF: Buffer is too small or empty. " +
|
|
503
|
+
"A valid PDF must be at least 5 bytes (PDF header).");
|
|
504
|
+
}
|
|
505
|
+
if (!PDFProcessor.isValidPDF(pdfBuffer)) {
|
|
506
|
+
throw new Error("Invalid PDF: File must start with %PDF- header. " +
|
|
507
|
+
"The provided buffer does not appear to be a valid PDF file.");
|
|
508
|
+
}
|
|
509
|
+
const sizeMB = pdfBuffer.length / (1024 * 1024);
|
|
510
|
+
if (sizeMB > PDF_LIMITS.MAX_SIZE_MB) {
|
|
511
|
+
throw new Error(`PDF too large for image conversion: ${sizeMB.toFixed(2)}MB exceeds ${PDF_LIMITS.MAX_SIZE_MB}MB limit. ` +
|
|
512
|
+
"Consider splitting the PDF or using a provider with native PDF support.");
|
|
513
|
+
}
|
|
514
|
+
return sizeMB;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Streaming variant of {@link convertToImages} (#302): yields each page's
|
|
518
|
+
* base64 PNG as soon as it renders instead of buffering the whole document,
|
|
519
|
+
* and reports progress via `options.onProgress`. A page that fails to render
|
|
520
|
+
* is yielded with `error` set (per-page isolation, #294) rather than aborting
|
|
521
|
+
* the stream. `convertToImages` is the batch wrapper over this contract.
|
|
522
|
+
*/
|
|
523
|
+
static async *convertToImagesStream(pdfBuffer, options) {
|
|
524
|
+
const startTime = Date.now();
|
|
525
|
+
const { scale = PDF_LIMITS.DEFAULT_SCALE, maxPages = PDF_LIMITS.DEFAULT_MAX_PAGES, format = "png", maxCanvasPixels = PDF_LIMITS.DEFAULT_MAX_CANVAS_PIXELS, password, onProgress, } = options || {};
|
|
526
|
+
PDFProcessor.validateImageConversionInput(pdfBuffer, {
|
|
527
|
+
format,
|
|
528
|
+
scale,
|
|
529
|
+
maxCanvasPixels,
|
|
530
|
+
});
|
|
531
|
+
const pdfToImgModule = await import("pdf-to-img");
|
|
532
|
+
const pdf = pdfToImgModule.pdf;
|
|
533
|
+
// #260: uniform downscale so the largest page stays under maxCanvasPixels.
|
|
534
|
+
let effectiveScale = scale;
|
|
535
|
+
const largestPixels = PDFProcessor.largestPagePixels(pdfBuffer, scale);
|
|
536
|
+
if (largestPixels > maxCanvasPixels) {
|
|
537
|
+
effectiveScale = scale * Math.sqrt(maxCanvasPixels / largestPixels);
|
|
538
|
+
}
|
|
539
|
+
const document = await pdf(pdfBuffer, {
|
|
540
|
+
scale: effectiveScale,
|
|
541
|
+
...(password ? { password } : {}),
|
|
542
|
+
});
|
|
543
|
+
const totalPages = document.length;
|
|
544
|
+
let converted = 0;
|
|
545
|
+
for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
|
|
546
|
+
if (maxPages !== undefined && pageNum - 1 >= maxPages) {
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const page = await document.getPage(pageNum);
|
|
551
|
+
const base64Image = page.toString("base64");
|
|
552
|
+
converted++;
|
|
553
|
+
if (onProgress) {
|
|
554
|
+
await onProgress({
|
|
555
|
+
pagesConverted: converted,
|
|
556
|
+
totalPages,
|
|
557
|
+
elapsedMs: Date.now() - startTime,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
yield {
|
|
561
|
+
pageIndex: pageNum,
|
|
562
|
+
image: base64Image,
|
|
563
|
+
imageSizeBytes: page.length,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
catch (pageError) {
|
|
567
|
+
const msg = pageError instanceof Error ? pageError.message : String(pageError);
|
|
568
|
+
logger.warn(`[PDF→Image] ⚠️ page ${pageNum} failed to render (stream): ${msg}`);
|
|
569
|
+
yield { pageIndex: pageNum, image: "", imageSizeBytes: 0, error: msg };
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
434
573
|
/**
|
|
435
574
|
* Convert a PDF file path to an array of base64 PNG images
|
|
436
575
|
*
|
|
@@ -466,7 +605,7 @@ export class PDFProcessor {
|
|
|
466
605
|
* @param scale - Scale factor
|
|
467
606
|
* @returns Estimated memory usage in MB
|
|
468
607
|
*/
|
|
469
|
-
static estimateConversionMemoryUsage(pdfSizeBytes, pageCount, scale =
|
|
608
|
+
static estimateConversionMemoryUsage(pdfSizeBytes, pageCount, scale = PDF_LIMITS.DEFAULT_SCALE) {
|
|
470
609
|
// Rough estimation:
|
|
471
610
|
// - Each page at scale 2 produces ~1-3MB PNG
|
|
472
611
|
// - MuPDF needs ~2x PDF size for processing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.95.
|
|
3
|
+
"version": "9.95.3",
|
|
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": {
|