()
| 122 | } |
| 123 | |
| 124 | export async function getImageFromClipboard(): Promise<ImageWithDimensions | null> { |
| 125 | // Fast path: native NSPasteboard reader (macOS only). Reads PNG bytes |
| 126 | // directly in-process and downsamples via CoreGraphics if over the |
| 127 | // dimension cap. ~5ms cold, sub-ms warm — vs. ~1.5s for the osascript |
| 128 | // path below. Throws if the native module is unavailable, in which case |
| 129 | // the catch block falls through to osascript. A `null` return from the |
| 130 | // native call is authoritative (clipboard has no image). |
| 131 | if ( |
| 132 | feature('NATIVE_CLIPBOARD_IMAGE') && |
| 133 | process.platform === 'darwin' && |
| 134 | getFeatureValue_CACHED_MAY_BE_STALE('tengu_collage_kaleidoscope', true) |
| 135 | ) { |
| 136 | try { |
| 137 | const { getNativeModule } = await import('image-processor-napi') |
| 138 | const readClipboard = getNativeModule()?.readClipboardImage |
| 139 | if (!readClipboard) { |
| 140 | throw new Error('native clipboard reader unavailable') |
| 141 | } |
| 142 | const native = readClipboard(IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT) |
| 143 | if (!native) { |
| 144 | return null |
| 145 | } |
| 146 | // The native path caps dimensions but not file size. A complex |
| 147 | // 2000×2000 PNG can still exceed the 3.75MB raw / 5MB base64 API |
| 148 | // limit — for that edge case, run through the same size-cap that |
| 149 | // the osascript path uses (degrades to JPEG if needed). Cheap if |
| 150 | // already under: just a sharp metadata read. |
| 151 | const buffer: Buffer = native.png |
| 152 | if (buffer.length > IMAGE_TARGET_RAW_SIZE) { |
| 153 | const resized = await maybeResizeAndDownsampleImageBuffer( |
| 154 | buffer, |
| 155 | buffer.length, |
| 156 | 'png', |
| 157 | ) |
| 158 | return { |
| 159 | base64: resized.buffer.toString('base64'), |
| 160 | mediaType: `image/${resized.mediaType}`, |
| 161 | // resized.dimensions sees the already-downsampled buffer; native knows the true originals. |
| 162 | dimensions: { |
| 163 | originalWidth: native.originalWidth, |
| 164 | originalHeight: native.originalHeight, |
| 165 | displayWidth: resized.dimensions?.displayWidth ?? native.width, |
| 166 | displayHeight: resized.dimensions?.displayHeight ?? native.height, |
| 167 | }, |
| 168 | } |
| 169 | } |
| 170 | return { |
| 171 | base64: buffer.toString('base64'), |
| 172 | mediaType: 'image/png', |
| 173 | dimensions: { |
| 174 | originalWidth: native.originalWidth, |
| 175 | originalHeight: native.originalHeight, |
| 176 | displayWidth: native.width, |
| 177 | displayHeight: native.height, |
| 178 | }, |
| 179 | } |
| 180 | } catch (e) { |
| 181 | logError(e as Error) |
no test coverage detected