@juspay/neurolink 12.22.0 → 12.22.1

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.
@@ -311,7 +311,7 @@ export const PDF_LIMITS = {
311
311
  DEFAULT_MAX_CANVAS_PIXELS: 16_777_216,
312
312
  // Floor for the downscaled render scale (#260 follow-up). A crafted/malformed
313
313
  // MediaBox can drive the estimated pixel count toward Infinity, which would
314
- // otherwise collapse `effectiveScale` to 0 and hand `pdf-to-img` a degenerate
314
+ // otherwise collapse `effectiveScale` to 0 and hand the renderer a degenerate
315
315
  // viewport. Never let the downscale branch go below this.
316
316
  MIN_EFFECTIVE_SCALE: 0.1,
317
317
  };
@@ -37,6 +37,10 @@ export declare const buildWireToolNameMaps: (names: readonly string[], reservedW
37
37
  * tool definitions). Used by the per-request max_tokens fit against a
38
38
  * RUNTIME-DISCOVERED context window — deliberately the same char-based
39
39
  * estimator the budget pipeline uses, so both layers agree.
40
+ *
41
+ * Image parts are billed at the flat IMAGE_TOKEN_ESTIMATE, never by the length
42
+ * of their base64 data URL: a PDF rendered to 10 page images otherwise
43
+ * stringifies to ~1.2M "tokens" and is rejected before it is ever sent.
40
44
  */
41
45
  export declare const estimateWireTokens: (messages: ReadonlyArray<OpenAICompatChatMessage>, tools: OpenAICompatChatTool[] | undefined, provider?: string) => number;
42
46
  export declare const safeStringify: (value: unknown) => string;
@@ -17,7 +17,7 @@ import { createParser } from "eventsource-parser";
17
17
  import { sanitizeToolName } from "../mcp/toolConverter.js";
18
18
  import { resolveSamplingParams } from "../models/modelRegistry.js";
19
19
  import { convertZodToJsonSchema, normalizeWireToolSchema, } from "../utils/schemaConversion.js";
20
- import { estimateTokens, TOKENS_PER_MESSAGE, } from "../utils/tokenEstimation.js";
20
+ import { estimateTokens, IMAGE_TOKEN_ESTIMATE, TOKENS_PER_MESSAGE, } from "../utils/tokenEstimation.js";
21
21
  export const stripTrailingSlash = (s) => s.replace(/\/+$/, "");
22
22
  // OpenAI-compatible wire tool names: first char letter/underscore (some
23
23
  // chat templates treat a leading digit or hyphen as invalid identifiers),
@@ -68,14 +68,29 @@ export const buildWireToolNameMaps = (names, reservedWireNames) => {
68
68
  * tool definitions). Used by the per-request max_tokens fit against a
69
69
  * RUNTIME-DISCOVERED context window — deliberately the same char-based
70
70
  * estimator the budget pipeline uses, so both layers agree.
71
+ *
72
+ * Image parts are billed at the flat IMAGE_TOKEN_ESTIMATE, never by the length
73
+ * of their base64 data URL: a PDF rendered to 10 page images otherwise
74
+ * stringifies to ~1.2M "tokens" and is rejected before it is ever sent.
71
75
  */
72
76
  export const estimateWireTokens = (messages, tools, provider) => {
73
77
  let total = 0;
74
78
  for (const message of messages) {
75
- const content = typeof message.content === "string"
76
- ? message.content
77
- : safeStringify(message.content);
78
- total += estimateTokens(content, provider) + TOKENS_PER_MESSAGE;
79
+ if (Array.isArray(message.content)) {
80
+ for (const part of message.content) {
81
+ total +=
82
+ part.type === "image_url"
83
+ ? IMAGE_TOKEN_ESTIMATE
84
+ : estimateTokens(part.text, provider);
85
+ }
86
+ total += TOKENS_PER_MESSAGE;
87
+ }
88
+ else {
89
+ const content = typeof message.content === "string"
90
+ ? message.content
91
+ : safeStringify(message.content);
92
+ total += estimateTokens(content, provider) + TOKENS_PER_MESSAGE;
93
+ }
79
94
  // tool_calls only exists on the assistant variant of the message union.
80
95
  const toolCalls = message.tool_calls;
81
96
  if (toolCalls) {
@@ -552,6 +552,19 @@ export type PDFImagePage = {
552
552
  /** Populated when this page failed to render (#294). */
553
553
  error?: string;
554
554
  };
555
+ /**
556
+ * A PDF opened for page rendering. Rendering goes through pdf-parse so the
557
+ * process loads exactly one pdfjs-dist copy — a second copy fails pdfjs's
558
+ * API-vs-Worker version check for whichever library loads second.
559
+ */
560
+ export type PDFRenderDocument = {
561
+ /** Total pages in the document. */
562
+ length: number;
563
+ /** Render a 1-based page to a PNG buffer. */
564
+ getPage: (pageNumber: number) => Promise<Buffer>;
565
+ /** Release pdfjs resources held by the document. */
566
+ destroy: () => Promise<void>;
567
+ };
555
568
  /**
556
569
  * A single PDF queued for multimodal message building, normalised from either
557
570
  * submission surface — `input.pdfFiles` or `input.content` with `type: "pdf"`
@@ -5,7 +5,10 @@
5
5
  * - Native PDF support for providers that accept PDF directly (Google AI, Vertex, OpenAI, Anthropic, Bedrock)
6
6
  * - PDF → Image conversion for providers that don't support native PDF (Azure, Mistral, Ollama)
7
7
  *
8
- * The conversion uses pdf-to-img package (MuPDF-based) for high-quality conversion.
8
+ * Page counting, text extraction and page rendering all go through pdf-parse,
9
+ * so the process only ever loads pdf-parse's pdfjs-dist. pdfjs keeps its Node
10
+ * worker in a process-wide global, and a second pdfjs version fails its
11
+ * API-vs-Worker version check against whichever copy loaded first.
9
12
  */
10
13
  import type { FileProcessingResult, PDFProcessorOptions, PDFProviderConfig, PDFImageConversionOptions, PDFImageConversionResult, PDFImagePage } from "../types/index.js";
11
14
  export declare class PDFProcessor {
@@ -40,6 +43,21 @@ export declare class PDFProcessor {
40
43
  * falls back to the regex estimate.
41
44
  */
42
45
  private static getAccuratePageCount;
46
+ /**
47
+ * Font and CMap directories of the pdfjs-dist copy pdf-parse itself loads,
48
+ * so non-embedded standard fonts and CJK text render instead of dropping
49
+ * out. Resolved from pdf-parse's location, never our own, to stay on that
50
+ * single copy. Undefined when resolution fails; rendering still works, with
51
+ * pdfjs falling back to its built-in font substitution.
52
+ */
53
+ private static pdfjsAssetDirs;
54
+ private static resolvePdfjsAssetDirs;
55
+ /**
56
+ * Open a PDF for page-by-page PNG rendering through pdf-parse. Rejects with
57
+ * pdf-parse's PasswordException for encrypted PDFs (#258), which the callers
58
+ * map to typed errors.
59
+ */
60
+ private static openForRendering;
43
61
  static estimateTokens(pageCount: number, mode?: "text-only" | "visual"): number;
44
62
  /**
45
63
  * Estimate the largest page's rendered pixel count from the PDF's MediaBox
@@ -96,7 +114,7 @@ export declare class PDFProcessor {
96
114
  *
97
115
  * NOT a wrapper of/over {@link convertToImages}, despite the similar
98
116
  * contract — the two are independent, parallel implementations (each does
99
- * its own `pdf-to-img` import, downscale calculation, and page loop)
117
+ * its own document open, downscale calculation, and page loop)
100
118
  * rather than one delegating to the other. Keep behavior changes (page
101
119
  * isolation, downscale, password handling) in sync across both by hand.
102
120
  */
@@ -113,7 +131,7 @@ export declare class PDFProcessor {
113
131
  * Check if PDF to image conversion is available
114
132
  * Useful for feature detection
115
133
  *
116
- * @returns true if pdf-to-img package is available
134
+ * @returns true if the pdf-parse renderer is available
117
135
  */
118
136
  static isImageConversionAvailable(): Promise<boolean>;
119
137
  /**
@@ -5,8 +5,13 @@
5
5
  * - Native PDF support for providers that accept PDF directly (Google AI, Vertex, OpenAI, Anthropic, Bedrock)
6
6
  * - PDF → Image conversion for providers that don't support native PDF (Azure, Mistral, Ollama)
7
7
  *
8
- * The conversion uses pdf-to-img package (MuPDF-based) for high-quality conversion.
8
+ * Page counting, text extraction and page rendering all go through pdf-parse,
9
+ * so the process only ever loads pdf-parse's pdfjs-dist. pdfjs keeps its Node
10
+ * worker in a process-wide global, and a second pdfjs version fails its
11
+ * API-vs-Worker version check against whichever copy loaded first.
9
12
  */
13
+ import { createRequire } from "node:module";
14
+ import path from "node:path";
10
15
  import { PDF_LIMITS } from "../core/constants.js";
11
16
  import { ErrorFactory } from "./errorHandling.js";
12
17
  import { logger } from "./logger.js";
@@ -297,6 +302,76 @@ export class PDFProcessor {
297
302
  return null;
298
303
  }
299
304
  }
305
+ /**
306
+ * Font and CMap directories of the pdfjs-dist copy pdf-parse itself loads,
307
+ * so non-embedded standard fonts and CJK text render instead of dropping
308
+ * out. Resolved from pdf-parse's location, never our own, to stay on that
309
+ * single copy. Undefined when resolution fails; rendering still works, with
310
+ * pdfjs falling back to its built-in font substitution.
311
+ */
312
+ static pdfjsAssetDirs;
313
+ static resolvePdfjsAssetDirs() {
314
+ if (PDFProcessor.pdfjsAssetDirs === undefined) {
315
+ try {
316
+ const pdfParseEntry = createRequire(import.meta.url).resolve("pdf-parse");
317
+ const pdfjsRoot = path.dirname(createRequire(pdfParseEntry).resolve("pdfjs-dist/package.json"));
318
+ PDFProcessor.pdfjsAssetDirs = {
319
+ standardFontDataUrl: path.join(pdfjsRoot, "standard_fonts") + path.sep,
320
+ cMapUrl: path.join(pdfjsRoot, "cmaps") + path.sep,
321
+ };
322
+ }
323
+ catch (error) {
324
+ logger.debug(`[PDF→Image] pdfjs font/CMap directories unavailable: ${error instanceof Error ? error.message : String(error)}`);
325
+ PDFProcessor.pdfjsAssetDirs = null;
326
+ }
327
+ }
328
+ return PDFProcessor.pdfjsAssetDirs ?? undefined;
329
+ }
330
+ /**
331
+ * Open a PDF for page-by-page PNG rendering through pdf-parse. Rejects with
332
+ * pdf-parse's PasswordException for encrypted PDFs (#258), which the callers
333
+ * map to typed errors.
334
+ */
335
+ static async openForRendering(pdfBuffer, scale, password) {
336
+ const { PDFParse } = await import("pdf-parse");
337
+ const parser = new PDFParse({
338
+ data: new Uint8Array(pdfBuffer),
339
+ isEvalSupported: false,
340
+ cMapPacked: true,
341
+ ...PDFProcessor.resolvePdfjsAssetDirs(),
342
+ ...(password ? { password } : {}),
343
+ });
344
+ try {
345
+ const { total } = await parser.getInfo();
346
+ return {
347
+ length: total,
348
+ getPage: async (pageNumber) => {
349
+ const shot = await parser.getScreenshot({
350
+ partial: [pageNumber],
351
+ scale,
352
+ imageBuffer: true,
353
+ imageDataUrl: false,
354
+ });
355
+ const page = shot.pages[0];
356
+ if (!page || page.data.length === 0) {
357
+ throw new Error(`page ${pageNumber} rendered no image data`);
358
+ }
359
+ return Buffer.from(page.data);
360
+ },
361
+ destroy: async () => {
362
+ await parser.destroy().catch(() => {
363
+ /* cleanup - ignore destroy errors */
364
+ });
365
+ },
366
+ };
367
+ }
368
+ catch (error) {
369
+ await parser.destroy().catch(() => {
370
+ /* cleanup - ignore destroy errors */
371
+ });
372
+ throw error;
373
+ }
374
+ }
300
375
  static estimateTokens(pageCount, mode = "visual") {
301
376
  if (mode === "text-only") {
302
377
  return Math.ceil((pageCount / 3) * 1000);
@@ -395,16 +470,14 @@ export class PDFProcessor {
395
470
  estimatedMemoryMB,
396
471
  sizeMB: Number(sizeMB.toFixed(2)),
397
472
  });
473
+ let document;
398
474
  try {
399
- // Dynamic import to avoid loading MuPDF binaries until needed
400
- const pdfToImgModule = await import("pdf-to-img");
401
- const pdf = pdfToImgModule.pdf;
402
475
  logger.debug("[PDF→Image] Starting PDF to image conversion", {
403
476
  bufferSize: pdfBuffer.length,
404
477
  scale,
405
478
  maxPages: maxPages || "all",
406
479
  });
407
- // #260: pre-flight page-size check WITHOUT rendering. pdf-to-img applies
480
+ // #260: pre-flight page-size check WITHOUT rendering. The renderer applies
408
481
  // `scale` uniformly with no per-page hook and no pixel guard, so a very
409
482
  // large page (e.g. an architectural drawing with a huge MediaBox) can
410
483
  // allocate gigabytes of canvas. Read the largest MediaBox from the PDF
@@ -416,7 +489,7 @@ export class PDFProcessor {
416
489
  const downscale = Math.sqrt(maxCanvasPixels / largestPixels);
417
490
  // Floor the result: an astronomically large (but now finite, see
418
491
  // `largestPagePixels`) pixel estimate would otherwise push `downscale`
419
- // — and therefore `effectiveScale` — toward 0, handing `pdf-to-img` a
492
+ // — and therefore `effectiveScale` — toward 0, handing the renderer a
420
493
  // degenerate viewport instead of a small-but-renderable page.
421
494
  effectiveScale = Math.max(PDF_LIMITS.MIN_EFFECTIVE_SCALE, scale * downscale);
422
495
  // Recompute the ratio actually applied (may differ from `downscale`
@@ -431,14 +504,9 @@ export class PDFProcessor {
431
504
  logger.warn(`[PDF→Image] ⚠️ ${msg}`);
432
505
  warnings.push(msg);
433
506
  }
434
- // Create PDF document (password forwarded for encrypted PDFs, #258).
435
- // pdf-to-img resolves `.length` (numPages) synchronously here and exposes
436
- // `.getPage(n)`, so we drive an indexed loop rather than the async
437
- // iterator — that lets one bad page be isolated instead of aborting all.
438
- const document = await pdf(pdfBuffer, {
439
- scale: effectiveScale,
440
- ...(password ? { password } : {}),
441
- });
507
+ // Password forwarded for encrypted PDFs (#258). An indexed getPage()
508
+ // loop lets one bad page be isolated instead of aborting all.
509
+ document = await PDFProcessor.openForRendering(pdfBuffer, effectiveScale, password);
442
510
  const totalPages = document.length;
443
511
  // #294: convert page-by-page with per-page isolation. A single page whose
444
512
  // canvas render throws (e.g. a degenerate MediaBox) no longer discards
@@ -518,6 +586,9 @@ export class PDFProcessor {
518
586
  cause: error,
519
587
  });
520
588
  }
589
+ finally {
590
+ await document?.destroy();
591
+ }
521
592
  }
522
593
  /**
523
594
  * Shared input validation for the image-conversion paths. Returns the PDF
@@ -568,7 +639,7 @@ export class PDFProcessor {
568
639
  *
569
640
  * NOT a wrapper of/over {@link convertToImages}, despite the similar
570
641
  * contract — the two are independent, parallel implementations (each does
571
- * its own `pdf-to-img` import, downscale calculation, and page loop)
642
+ * its own document open, downscale calculation, and page loop)
572
643
  * rather than one delegating to the other. Keep behavior changes (page
573
644
  * isolation, downscale, password handling) in sync across both by hand.
574
645
  */
@@ -581,8 +652,6 @@ export class PDFProcessor {
581
652
  maxCanvasPixels,
582
653
  maxPages,
583
654
  });
584
- const pdfToImgModule = await import("pdf-to-img");
585
- const pdf = pdfToImgModule.pdf;
586
655
  // #260: uniform downscale so the largest page stays under maxCanvasPixels.
587
656
  let effectiveScale = scale;
588
657
  const largestPixels = PDFProcessor.largestPagePixels(pdfBuffer, scale);
@@ -596,10 +665,7 @@ export class PDFProcessor {
596
665
  // being yielded as a page error.
597
666
  let document;
598
667
  try {
599
- document = await pdf(pdfBuffer, {
600
- scale: effectiveScale,
601
- ...(password ? { password } : {}),
602
- });
668
+ document = await PDFProcessor.openForRendering(pdfBuffer, effectiveScale, password);
603
669
  }
604
670
  catch (error) {
605
671
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -617,33 +683,43 @@ export class PDFProcessor {
617
683
  }
618
684
  const totalPages = document.length;
619
685
  let converted = 0;
620
- for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
621
- if (maxPages !== undefined && pageNum - 1 >= maxPages) {
622
- break;
623
- }
624
- try {
625
- const page = await document.getPage(pageNum);
626
- const base64Image = page.toString("base64");
627
- converted++;
628
- if (onProgress) {
629
- await onProgress({
630
- pagesConverted: converted,
631
- totalPages,
632
- elapsedMs: Date.now() - startTime,
633
- });
686
+ try {
687
+ for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
688
+ if (maxPages !== undefined && pageNum - 1 >= maxPages) {
689
+ break;
690
+ }
691
+ try {
692
+ const page = await document.getPage(pageNum);
693
+ const base64Image = page.toString("base64");
694
+ converted++;
695
+ if (onProgress) {
696
+ await onProgress({
697
+ pagesConverted: converted,
698
+ totalPages,
699
+ elapsedMs: Date.now() - startTime,
700
+ });
701
+ }
702
+ yield {
703
+ pageIndex: pageNum,
704
+ image: base64Image,
705
+ imageSizeBytes: page.length,
706
+ };
707
+ }
708
+ catch (pageError) {
709
+ const msg = pageError instanceof Error ? pageError.message : String(pageError);
710
+ logger.warn(`[PDF→Image] ⚠️ page ${pageNum} failed to render (stream): ${msg}`);
711
+ yield {
712
+ pageIndex: pageNum,
713
+ image: "",
714
+ imageSizeBytes: 0,
715
+ error: msg,
716
+ };
634
717
  }
635
- yield {
636
- pageIndex: pageNum,
637
- image: base64Image,
638
- imageSizeBytes: page.length,
639
- };
640
- }
641
- catch (pageError) {
642
- const msg = pageError instanceof Error ? pageError.message : String(pageError);
643
- logger.warn(`[PDF→Image] ⚠️ page ${pageNum} failed to render (stream): ${msg}`);
644
- yield { pageIndex: pageNum, image: "", imageSizeBytes: 0, error: msg };
645
718
  }
646
719
  }
720
+ finally {
721
+ await document.destroy();
722
+ }
647
723
  }
648
724
  /**
649
725
  * Convert a PDF file path to an array of base64 PNG images
@@ -661,11 +737,11 @@ export class PDFProcessor {
661
737
  * Check if PDF to image conversion is available
662
738
  * Useful for feature detection
663
739
  *
664
- * @returns true if pdf-to-img package is available
740
+ * @returns true if the pdf-parse renderer is available
665
741
  */
666
742
  static async isImageConversionAvailable() {
667
743
  try {
668
- await import("pdf-to-img");
744
+ await import("pdf-parse");
669
745
  return true;
670
746
  }
671
747
  catch {
@@ -683,7 +759,7 @@ export class PDFProcessor {
683
759
  static estimateConversionMemoryUsage(pdfSizeBytes, pageCount, scale = PDF_LIMITS.DEFAULT_SCALE) {
684
760
  // Rough estimation:
685
761
  // - Each page at scale 2 produces ~1-3MB PNG
686
- // - MuPDF needs ~2x PDF size for processing
762
+ // - pdfjs needs ~2x PDF size for processing
687
763
  // - Output images need ~2MB per page on average
688
764
  const pdfProcessingMB = (pdfSizeBytes / (1024 * 1024)) * 2;
689
765
  const outputImagesMB = pageCount * 2 * scale;