Flatten transparent product images to white and allow cropper unzoom.
PNG/WebP uploads no longer bake to black on JPEG; product croppers can zoom out with white padding, and SEO tools move into the product overflow menu. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
b743c09e4d
commit
06e2442aaf
@@ -9,34 +9,63 @@ function createImage(url: string): Promise<HTMLImageElement> {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Crop `imageSrc` to `pixelCrop`. Areas outside the source image use `fillColor`
|
||||
* when provided. JPEG always defaults to white so transparent PNG/WebP uploads
|
||||
* do not become black.
|
||||
*/
|
||||
export async function getCroppedImage(
|
||||
imageSrc: string,
|
||||
pixelCrop: Area,
|
||||
format: 'jpeg' | 'png' = 'jpeg',
|
||||
fillColor?: string,
|
||||
): Promise<string> {
|
||||
const image = await createImage(imageSrc)
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('Could not get canvas context')
|
||||
|
||||
canvas.width = pixelCrop.width
|
||||
canvas.height = pixelCrop.height
|
||||
const outW = Math.max(1, Math.round(pixelCrop.width))
|
||||
const outH = Math.max(1, Math.round(pixelCrop.height))
|
||||
canvas.width = outW
|
||||
canvas.height = outH
|
||||
|
||||
if (format === 'png') {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
const background = format === 'jpeg' ? (fillColor ?? '#ffffff') : fillColor
|
||||
if (background) {
|
||||
ctx.fillStyle = background
|
||||
ctx.fillRect(0, 0, outW, outH)
|
||||
} else {
|
||||
ctx.clearRect(0, 0, outW, outH)
|
||||
}
|
||||
|
||||
ctx.drawImage(
|
||||
image,
|
||||
pixelCrop.x,
|
||||
pixelCrop.y,
|
||||
pixelCrop.width,
|
||||
pixelCrop.height,
|
||||
0,
|
||||
0,
|
||||
pixelCrop.width,
|
||||
pixelCrop.height,
|
||||
)
|
||||
const srcX = pixelCrop.x
|
||||
const srcY = pixelCrop.y
|
||||
const srcW = pixelCrop.width
|
||||
const srcH = pixelCrop.height
|
||||
|
||||
const intersectLeft = Math.max(0, srcX)
|
||||
const intersectTop = Math.max(0, srcY)
|
||||
const intersectRight = Math.min(image.naturalWidth, srcX + srcW)
|
||||
const intersectBottom = Math.min(image.naturalHeight, srcY + srcH)
|
||||
|
||||
const intersectW = intersectRight - intersectLeft
|
||||
const intersectH = intersectBottom - intersectTop
|
||||
|
||||
if (intersectW > 0 && intersectH > 0) {
|
||||
const destX = intersectLeft - srcX
|
||||
const destY = intersectTop - srcY
|
||||
ctx.drawImage(
|
||||
image,
|
||||
intersectLeft,
|
||||
intersectTop,
|
||||
intersectW,
|
||||
intersectH,
|
||||
destX,
|
||||
destY,
|
||||
intersectW,
|
||||
intersectH,
|
||||
)
|
||||
}
|
||||
|
||||
if (format === 'png') {
|
||||
return canvas.toDataURL('image/png')
|
||||
|
||||
@@ -37,15 +37,25 @@ export function dataUrlToFile(dataUrl: string, filename: string): File {
|
||||
return new File([copy], filename, { type: mime })
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a data URL (or image URL) for upload. When the target is JPEG,
|
||||
* transparent PNG/WebP pixels are flattened onto white — never black.
|
||||
*/
|
||||
export async function ensureUploadFile(dataUrl: string, filename: string): Promise<File> {
|
||||
const wantsPng = filename.toLowerCase().endsWith('.png')
|
||||
|
||||
if (dataUrl.startsWith('data:')) {
|
||||
const mime = dataUrl.slice(5, dataUrl.indexOf(';'))
|
||||
const normalized = mime === 'image/jpg' ? 'image/jpeg' : mime
|
||||
|
||||
if (ALLOWED_IMAGE_TYPES.has(normalized)) {
|
||||
const file = dataUrlToFile(dataUrl, filename)
|
||||
if (file.size > 0) {
|
||||
return file
|
||||
const alreadyPng = wantsPng && normalized === 'image/png'
|
||||
const alreadyJpeg = !wantsPng && normalized === 'image/jpeg'
|
||||
if (alreadyPng || alreadyJpeg) {
|
||||
const file = dataUrlToFile(dataUrl, filename)
|
||||
if (file.size > 0) {
|
||||
return file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,12 +67,14 @@ export async function ensureUploadFile(dataUrl: string, filename: string): Promi
|
||||
throw new Error('Could not prepare image for upload')
|
||||
}
|
||||
|
||||
const wantsPng = filename.toLowerCase().endsWith('.png')
|
||||
canvas.width = image.naturalWidth
|
||||
canvas.height = image.naturalHeight
|
||||
|
||||
if (wantsPng) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
} else {
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
ctx.drawImage(image, 0, 0)
|
||||
|
||||
Reference in New Issue
Block a user