index.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. export type ClipboardImageResult = {
  2. png: Buffer
  3. originalWidth: number
  4. originalHeight: number
  5. width: number
  6. height: number
  7. }
  8. // Clipboard functions are macOS-only and only present in darwin binaries;
  9. // older/non-darwin binaries built before this addition won't export them.
  10. // Typed as optional so callers can guard. These property names appear only
  11. // in type-space here; all runtime property access lives in src/ behind
  12. // feature() so they tree-shake out of builds that don't want them.
  13. export type NativeModule = {
  14. processImage: (input: Buffer) => Promise<ImageProcessor>
  15. readClipboardImage?: (maxWidth: number, maxHeight: number) => ClipboardImageResult | null
  16. hasClipboardImage?: () => boolean
  17. }
  18. // Lazy: defers dlopen until first call. The .node binary links against
  19. // CoreGraphics/ImageIO on darwin; resolving that at module-eval time blocks
  20. // startup because imagePaste.ts pulls this into the REPL chunk via static
  21. // import. Same pattern as audio-capture-src/index.ts.
  22. let cachedModule: NativeModule | null = null
  23. let loadAttempted = false
  24. // Raw binding accessor. Callers that need optional exports (e.g. clipboard
  25. // functions) reach through this; keeping the wrappers on the caller side lets
  26. // feature() tree-shake the property access strings out of external builds.
  27. export function getNativeModule(): NativeModule | null {
  28. if (loadAttempted) return cachedModule
  29. loadAttempted = true
  30. try {
  31. // eslint-disable-next-line @typescript-eslint/no-require-imports
  32. cachedModule = require('../../image-processor.node')
  33. } catch {
  34. cachedModule = null
  35. }
  36. return cachedModule
  37. }
  38. interface ImageProcessor {
  39. metadata(): { width: number; height: number; format: string }
  40. resize(
  41. width: number,
  42. height: number,
  43. options?: { fit?: string; withoutEnlargement?: boolean },
  44. ): ImageProcessor
  45. jpeg(quality?: number): ImageProcessor
  46. png(options?: {
  47. compressionLevel?: number
  48. palette?: boolean
  49. colors?: number
  50. }): ImageProcessor
  51. webp(quality?: number): ImageProcessor
  52. toBuffer(): Promise<Buffer>
  53. }
  54. interface SharpInstance {
  55. metadata(): Promise<{ width: number; height: number; format: string }>
  56. resize(
  57. width: number,
  58. height: number,
  59. options?: { fit?: string; withoutEnlargement?: boolean },
  60. ): SharpInstance
  61. jpeg(options?: { quality?: number }): SharpInstance
  62. png(options?: {
  63. compressionLevel?: number
  64. palette?: boolean
  65. colors?: number
  66. }): SharpInstance
  67. webp(options?: { quality?: number }): SharpInstance
  68. toBuffer(): Promise<Buffer>
  69. }
  70. // Factory function that matches sharp's API
  71. export function sharp(input: Buffer): SharpInstance {
  72. let processorPromise: Promise<ImageProcessor> | null = null
  73. // Create a chain of operations
  74. const operations: Array<(proc: ImageProcessor) => void> = []
  75. // Track how many operations have been applied to avoid re-applying
  76. let appliedOperationsCount = 0
  77. // Get or create the processor (without applying operations)
  78. async function ensureProcessor(): Promise<ImageProcessor> {
  79. if (!processorPromise) {
  80. processorPromise = (async () => {
  81. const mod = getNativeModule()
  82. if (!mod) {
  83. throw new Error('Native image processor module not available')
  84. }
  85. return mod.processImage(input)
  86. })()
  87. }
  88. return processorPromise
  89. }
  90. // Apply any pending operations to the processor
  91. function applyPendingOperations(proc: ImageProcessor): void {
  92. for (let i = appliedOperationsCount; i < operations.length; i++) {
  93. const op = operations[i]
  94. if (op) {
  95. op(proc)
  96. }
  97. }
  98. appliedOperationsCount = operations.length
  99. }
  100. const instance: SharpInstance = {
  101. async metadata() {
  102. const proc = await ensureProcessor()
  103. return proc.metadata()
  104. },
  105. resize(
  106. width: number,
  107. height: number,
  108. options?: { fit?: string; withoutEnlargement?: boolean },
  109. ) {
  110. operations.push(proc => {
  111. proc.resize(width, height, options)
  112. })
  113. return instance
  114. },
  115. jpeg(options?: { quality?: number }) {
  116. operations.push(proc => {
  117. proc.jpeg(options?.quality)
  118. })
  119. return instance
  120. },
  121. png(options?: {
  122. compressionLevel?: number
  123. palette?: boolean
  124. colors?: number
  125. }) {
  126. operations.push(proc => {
  127. proc.png(options)
  128. })
  129. return instance
  130. },
  131. webp(options?: { quality?: number }) {
  132. operations.push(proc => {
  133. proc.webp(options?.quality)
  134. })
  135. return instance
  136. },
  137. async toBuffer() {
  138. const proc = await ensureProcessor()
  139. applyPendingOperations(proc)
  140. return proc.toBuffer()
  141. },
  142. }
  143. return instance
  144. }
  145. export default sharp