80 lines
2.7 KiB
TypeScript
80 lines
2.7 KiB
TypeScript
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { extname, join } from 'node:path'
|
|
import { runFfmpeg } from '~/server/utils/ffmpeg'
|
|
import { imageDimensions } from '~/server/utils/resolution'
|
|
import { padExtents, parseImageAspect, type ImageAspectId } from '~/utils/imageAspect'
|
|
|
|
export type PaddedImage = { filename: string; data: Buffer; type?: string }
|
|
|
|
function sourceExt(filename: string, type?: string) {
|
|
const fromName = extname(filename || '').toLowerCase()
|
|
if (fromName === '.jpg' || fromName === '.jpeg' || fromName === '.png' || fromName === '.webp') return fromName
|
|
if (type?.includes('jpeg')) return '.jpg'
|
|
if (type?.includes('webp')) return '.webp'
|
|
return '.png'
|
|
}
|
|
|
|
export async function applyImageFill(opts: {
|
|
image: PaddedImage
|
|
mask?: PaddedImage | null
|
|
aspect: string
|
|
}): Promise<{ image: PaddedImage; mask?: PaddedImage; padded: boolean; aspect: ImageAspectId }> {
|
|
const aspect = parseImageAspect(opts.aspect)
|
|
if (aspect === 'auto') {
|
|
return { image: opts.image, mask: opts.mask || undefined, padded: false, aspect }
|
|
}
|
|
const size = imageDimensions(opts.image.data)
|
|
if (!size?.width || !size.height) {
|
|
return { image: opts.image, mask: opts.mask || undefined, padded: false, aspect }
|
|
}
|
|
const box = padExtents(size.width, size.height, aspect)
|
|
if (!box) {
|
|
return { image: opts.image, mask: opts.mask || undefined, padded: false, aspect }
|
|
}
|
|
|
|
const dir = mkdtempSync(join(tmpdir(), 'aigen-pad-'))
|
|
try {
|
|
const srcPath = join(dir, `src${sourceExt(opts.image.filename, opts.image.type)}`)
|
|
const outPath = join(dir, 'out.png')
|
|
writeFileSync(srcPath, opts.image.data)
|
|
await runFfmpeg([
|
|
'-y',
|
|
'-i', srcPath,
|
|
'-vf', `pad=${box.canvasW}:${box.canvasH}:${box.x}:${box.y}:black`,
|
|
'-frames:v', '1',
|
|
outPath
|
|
])
|
|
const padded: PaddedImage = {
|
|
filename: 'padded.png',
|
|
data: readFileSync(outPath),
|
|
type: 'image/png'
|
|
}
|
|
|
|
if (!opts.mask?.data?.length) {
|
|
return { image: padded, padded: true, aspect }
|
|
}
|
|
|
|
const userPath = join(dir, `mask${sourceExt(opts.mask.filename, opts.mask.type)}`)
|
|
const maskPath = join(dir, 'pad-mask.png')
|
|
writeFileSync(userPath, opts.mask.data)
|
|
await runFfmpeg([
|
|
'-y',
|
|
'-f', 'lavfi',
|
|
'-i', `color=c=white:s=${box.canvasW}x${box.canvasH}:d=1:r=1`,
|
|
'-i', userPath,
|
|
'-filter_complex', `[1]scale=${box.srcW}:${box.srcH}[m];[0][m]overlay=${box.x}:${box.y}`,
|
|
'-frames:v', '1',
|
|
maskPath
|
|
])
|
|
return {
|
|
image: padded,
|
|
mask: { filename: 'pad-mask.png', data: readFileSync(maskPath), type: 'image/png' },
|
|
padded: true,
|
|
aspect
|
|
}
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
}
|