feat: add eager image processing functions (generateAllSizes, convertOriginalToWebp)
This commit is contained in:
@@ -97,3 +97,65 @@ describe('image-resize', () => {
|
|||||||
await fs.promises.unlink(first.path)
|
await fs.promises.unlink(first.path)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('eager image processing', () => {
|
||||||
|
it('generateAllSizes creates all width+format combinations', async () => {
|
||||||
|
const { generateAllSizes } = await import('../image-resize.js')
|
||||||
|
const sharp = (await import('sharp')).default
|
||||||
|
const uuid = 'test-eager-uuid-1'
|
||||||
|
const testImagePath = path.join(UPLOADS_DIR, `${uuid}.png`)
|
||||||
|
await sharp({ create: { width: 2000, height: 1500, channels: 3, background: { r: 255, g: 0, b: 0 } } })
|
||||||
|
.png()
|
||||||
|
.toFile(testImagePath)
|
||||||
|
|
||||||
|
await generateAllSizes(uuid, '', testImagePath)
|
||||||
|
|
||||||
|
const cacheDir = path.join(UPLOADS_DIR, '.cache')
|
||||||
|
for (const width of [320, 640, 1024, 1600]) {
|
||||||
|
for (const format of ['avif', 'webp']) {
|
||||||
|
const cachePath = path.join(cacheDir, `${uuid}_w${width}.${format}`)
|
||||||
|
const exists = await fs.promises.access(cachePath).then(() => true).catch(() => false)
|
||||||
|
expect(exists).toBe(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await fs.promises.unlink(testImagePath)
|
||||||
|
for (const width of [320, 640, 1024, 1600]) {
|
||||||
|
for (const format of ['avif', 'webp']) {
|
||||||
|
const cachePath = path.join(cacheDir, `${uuid}_w${width}.${format}`)
|
||||||
|
try {
|
||||||
|
await fs.promises.unlink(cachePath)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('convertOriginalToWebp converts and deletes original', async () => {
|
||||||
|
const { convertOriginalToWebp } = await import('../image-resize.js')
|
||||||
|
const sharp = (await import('sharp')).default
|
||||||
|
const uuid = 'test-eager-uuid-2'
|
||||||
|
const testImagePath = path.join(UPLOADS_DIR, `${uuid}.png`)
|
||||||
|
await sharp({ create: { width: 800, height: 600, channels: 3, background: { r: 0, g: 255, b: 0 } } })
|
||||||
|
.png()
|
||||||
|
.toFile(testImagePath)
|
||||||
|
|
||||||
|
const result = await convertOriginalToWebp(uuid, '')
|
||||||
|
|
||||||
|
expect(result).toBe(`/uploads/${uuid}.webp`)
|
||||||
|
const pngExists = await fs.promises.access(testImagePath).then(() => true).catch(() => false)
|
||||||
|
expect(pngExists).toBe(false)
|
||||||
|
const webpPath = path.join(UPLOADS_DIR, `${uuid}.webp`)
|
||||||
|
const webpExists = await fs.promises.access(webpPath).then(() => true).catch(() => false)
|
||||||
|
expect(webpExists).toBe(true)
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
try {
|
||||||
|
await fs.promises.unlink(webpPath)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -64,3 +64,56 @@ export async function getOrCreateResized(uuid, width, format, subdir = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { VALID_WIDTHS, SUPPORTED_FORMATS }
|
export { VALID_WIDTHS, SUPPORTED_FORMATS }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate all resize widths in AVIF + WebP for eager processing.
|
||||||
|
* @param {string} uuid - UUID without extension
|
||||||
|
* @param {string} subdir - Subdirectory (e.g., 'reviews') or empty
|
||||||
|
* @param {string} originalPath - Full path to the original file
|
||||||
|
*/
|
||||||
|
export async function generateAllSizes(uuid, subdir, originalPath) {
|
||||||
|
const cacheSubdir = subdir ? subdir : ''
|
||||||
|
const cacheDir = path.join(CACHE_DIR, cacheSubdir)
|
||||||
|
await fs.promises.mkdir(cacheDir, { recursive: true })
|
||||||
|
|
||||||
|
const sharp = (await import('sharp')).default
|
||||||
|
|
||||||
|
for (const width of VALID_WIDTHS) {
|
||||||
|
for (const format of SUPPORTED_FORMATS) {
|
||||||
|
const cacheFileName = `${uuid}_w${width}.${format}`
|
||||||
|
const cachePath = path.join(CACHE_DIR, cacheSubdir, cacheFileName)
|
||||||
|
|
||||||
|
const pipeline = sharp(originalPath).resize(width, null, { withoutEnlargement: true })
|
||||||
|
const options = format === 'avif' ? { quality: 75, effort: 4 } : { quality: 80 }
|
||||||
|
await pipeline[format](options).toFile(cachePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert original file to WebP and delete the source file.
|
||||||
|
* @param {string} uuid - UUID without extension
|
||||||
|
* @param {string} subdir - Subdirectory (e.g., 'reviews') or empty
|
||||||
|
* @returns {string} New URL path like `/uploads/<uuid>.webp`
|
||||||
|
*/
|
||||||
|
export async function convertOriginalToWebp(uuid, subdir) {
|
||||||
|
const uploadsDir = path.join(process.cwd(), 'uploads')
|
||||||
|
const targetDir = subdir ? path.join(uploadsDir, subdir) : uploadsDir
|
||||||
|
|
||||||
|
const originalPath = await findOriginalFile(uuid, subdir)
|
||||||
|
if (!originalPath) {
|
||||||
|
throw new Error(`Original file not found for UUID: ${uuid}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalExt = path.extname(originalPath).toLowerCase()
|
||||||
|
const webpPath = path.join(targetDir, `${uuid}.webp`)
|
||||||
|
|
||||||
|
const sharp = (await import('sharp')).default
|
||||||
|
await sharp(originalPath).webp({ quality: 80 }).toFile(webpPath)
|
||||||
|
|
||||||
|
if (originalExt !== '.webp') {
|
||||||
|
await fs.promises.unlink(originalPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return subdir ? `/uploads/${subdir}/${uuid}.webp` : `/uploads/${uuid}.webp`
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user