Merge pull request #211 from joetam/fix-image-paste-stubs

fix linux clipboard image paste for jpeg/gif/webp
This commit is contained in:
Kevin Codex
2026-04-03 08:55:50 +08:00
committed by GitHub
2 changed files with 57 additions and 10 deletions

View File

@@ -34,6 +34,17 @@ type SharpCreator = (options: SharpCreatorOptions) => SharpInstance
let imageProcessorModule: { default: SharpFunction } | null = null let imageProcessorModule: { default: SharpFunction } | null = null
let imageCreatorModule: { default: SharpCreator } | null = null let imageCreatorModule: { default: SharpCreator } | null = null
/**
* Error thrown when no image processor is available (e.g., in the open build
* where sharp and image-processor-napi are stubbed out).
*/
export class ImageProcessorUnavailableError extends Error {
constructor() {
super('No image processor available (sharp is not installed)')
this.name = 'ImageProcessorUnavailableError'
}
}
export async function getImageProcessor(): Promise<SharpFunction> { export async function getImageProcessor(): Promise<SharpFunction> {
if (imageProcessorModule) { if (imageProcessorModule) {
return imageProcessorModule.default return imageProcessorModule.default
@@ -44,10 +55,14 @@ export async function getImageProcessor(): Promise<SharpFunction> {
try { try {
// Use the native image processor module // Use the native image processor module
const imageProcessor = await import('image-processor-napi') const imageProcessor = await import('image-processor-napi')
if ((imageProcessor as { __stub?: boolean }).__stub) {
throw new ImageProcessorUnavailableError()
}
const sharp = imageProcessor.sharp || imageProcessor.default const sharp = imageProcessor.sharp || imageProcessor.default
imageProcessorModule = { default: sharp } imageProcessorModule = { default: sharp }
return sharp return sharp
} catch { } catch (e) {
if (e instanceof ImageProcessorUnavailableError) throw e
// Fall back to sharp if native module is not available // Fall back to sharp if native module is not available
// biome-ignore lint/suspicious/noConsole: intentional warning // biome-ignore lint/suspicious/noConsole: intentional warning
console.warn( console.warn(
@@ -58,12 +73,20 @@ export async function getImageProcessor(): Promise<SharpFunction> {
// Use sharp for non-bundled builds or as fallback. // Use sharp for non-bundled builds or as fallback.
// Single structural cast: our SharpFunction is a subset of sharp's actual type surface. // Single structural cast: our SharpFunction is a subset of sharp's actual type surface.
try {
const imported = (await import( const imported = (await import(
'sharp' 'sharp'
)) as unknown as MaybeDefault<SharpFunction> )) as unknown as MaybeDefault<SharpFunction> & { __stub?: boolean }
const sharp = unwrapDefault(imported) if (imported && (imported as { __stub?: boolean }).__stub) {
throw new ImageProcessorUnavailableError()
}
const sharp = unwrapDefault(imported as MaybeDefault<SharpFunction>)
imageProcessorModule = { default: sharp } imageProcessorModule = { default: sharp }
return sharp return sharp
} catch (e) {
if (e instanceof ImageProcessorUnavailableError) throw e
throw new ImageProcessorUnavailableError()
}
} }
/** /**

View File

@@ -28,6 +28,31 @@ type SupportedPlatform = 'darwin' | 'linux' | 'win32'
// Threshold in characters for when to consider text a "large paste" // Threshold in characters for when to consider text a "large paste"
export const PASTE_THRESHOLD = 800 export const PASTE_THRESHOLD = 800
export const LINUX_CLIPBOARD_IMAGE_MIME_TYPES = [
'image/png',
'image/jpeg',
'image/jpg',
'image/gif',
'image/webp',
'image/bmp',
]
export function buildLinuxClipboardCheckCommand(): string {
const mimePattern = LINUX_CLIPBOARD_IMAGE_MIME_TYPES.map(mimeType =>
mimeType.replace('/', '\\/'),
).join('|')
return `xclip -selection clipboard -t TARGETS -o 2>/dev/null | grep -E "${mimePattern}" || wl-paste -l 2>/dev/null | grep -E "${mimePattern}"`
}
export function buildLinuxClipboardSaveCommand(screenshotPath: string): string {
return LINUX_CLIPBOARD_IMAGE_MIME_TYPES.flatMap(mimeType => [
`xclip -selection clipboard -t ${mimeType} -o > "${screenshotPath}" 2>/dev/null`,
`wl-paste --type ${mimeType} > "${screenshotPath}" 2>/dev/null`,
]).join(' || ')
}
function getClipboardCommands() { function getClipboardCommands() {
const platform = process.platform as SupportedPlatform const platform = process.platform as SupportedPlatform
@@ -62,9 +87,8 @@ function getClipboardCommands() {
deleteFile: `rm -f "${screenshotPath}"`, deleteFile: `rm -f "${screenshotPath}"`,
}, },
linux: { linux: {
checkImage: checkImage: buildLinuxClipboardCheckCommand(),
'xclip -selection clipboard -t TARGETS -o 2>/dev/null | grep -E "image/(png|jpeg|jpg|gif|webp|bmp)" || wl-paste -l 2>/dev/null | grep -E "image/(png|jpeg|jpg|gif|webp|bmp)"', saveImage: buildLinuxClipboardSaveCommand(screenshotPath),
saveImage: `xclip -selection clipboard -t image/png -o > "${screenshotPath}" 2>/dev/null || wl-paste --type image/png > "${screenshotPath}" 2>/dev/null || xclip -selection clipboard -t image/bmp -o > "${screenshotPath}" 2>/dev/null || wl-paste --type image/bmp > "${screenshotPath}"`,
getPath: getPath:
'xclip -selection clipboard -t text/plain -o 2>/dev/null || wl-paste 2>/dev/null', 'xclip -selection clipboard -t text/plain -o 2>/dev/null || wl-paste 2>/dev/null',
deleteFile: `rm -f "${screenshotPath}"`, deleteFile: `rm -f "${screenshotPath}"`,