/ tools / FileReadTool / imageProcessor.ts
imageProcessor.ts
 1  import type { Buffer } from 'buffer'
 2  import { isInBundledMode } from '../../utils/bundledMode.js'
 3  
 4  export type SharpInstance = {
 5    metadata(): Promise<{ width: number; height: number; format: string }>
 6    resize(
 7      width: number,
 8      height: number,
 9      options?: { fit?: string; withoutEnlargement?: boolean },
10    ): SharpInstance
11    jpeg(options?: { quality?: number }): SharpInstance
12    png(options?: {
13      compressionLevel?: number
14      palette?: boolean
15      colors?: number
16    }): SharpInstance
17    webp(options?: { quality?: number }): SharpInstance
18    toBuffer(): Promise<Buffer>
19  }
20  
21  export type SharpFunction = (input: Buffer) => SharpInstance
22  
23  type SharpCreatorOptions = {
24    create: {
25      width: number
26      height: number
27      channels: 3 | 4
28      background: { r: number; g: number; b: number }
29    }
30  }
31  
32  type SharpCreator = (options: SharpCreatorOptions) => SharpInstance
33  
34  let imageProcessorModule: { default: SharpFunction } | null = null
35  let imageCreatorModule: { default: SharpCreator } | null = null
36  
37  export async function getImageProcessor(): Promise<SharpFunction> {
38    if (imageProcessorModule) {
39      return imageProcessorModule.default
40    }
41  
42    if (isInBundledMode()) {
43      // Try to load the native image processor first
44      try {
45        // Use the native image processor module
46        const imageProcessor = await import('image-processor-napi')
47        const sharp = imageProcessor.sharp || imageProcessor.default
48        imageProcessorModule = { default: sharp }
49        return sharp
50      } catch {
51        // Fall back to sharp if native module is not available
52        // biome-ignore lint/suspicious/noConsole: intentional warning
53        console.warn(
54          'Native image processor not available, falling back to sharp',
55        )
56      }
57    }
58  
59    // Use sharp for non-bundled builds or as fallback.
60    // Single structural cast: our SharpFunction is a subset of sharp's actual type surface.
61    const imported = (await import(
62      'sharp'
63    )) as unknown as MaybeDefault<SharpFunction>
64    const sharp = unwrapDefault(imported)
65    imageProcessorModule = { default: sharp }
66    return sharp
67  }
68  
69  /**
70   * Get image creator for generating new images from scratch.
71   * Note: image-processor-napi doesn't support image creation,
72   * so this always uses sharp directly.
73   */
74  export async function getImageCreator(): Promise<SharpCreator> {
75    if (imageCreatorModule) {
76      return imageCreatorModule.default
77    }
78  
79    const imported = (await import(
80      'sharp'
81    )) as unknown as MaybeDefault<SharpCreator>
82    const sharp = unwrapDefault(imported)
83    imageCreatorModule = { default: sharp }
84    return sharp
85  }
86  
87  // Dynamic import shape varies by module interop mode — ESM yields { default: fn }, CJS yields fn directly.
88  type MaybeDefault<T> = T | { default: T }
89  
90  function unwrapDefault<T extends (...args: never[]) => unknown>(
91    mod: MaybeDefault<T>,
92  ): T {
93    return typeof mod === 'function' ? mod : mod.default
94  }