Достаточно частая задача веб разработчика — нарезать картинки. Предлагаю вашему вниманию готовое решение, используя Serverless framework + Lambda + S3.

Лямбда функция слушает событие загрузки файла в S3 и запускается. На входе функции мы можем извлечь имя S3 и ключ файла. Далее алгоритм такой: скачиваем файл в лямбду, нарезаем изображения и загружаем их в другой S3. Очень важно загружать в другой, потому что при загрузке в тот же S3 вызовется лямбда опять и будет нарезать уже нарезанные картинки (бесконечная рекурсия, за которую нужно будет заплатить). Это все в теории. Переходим к практике.
Для нарезки изображений будем использовать ffmpeg. Вот пример как можно сделать изображение уменьшенное и обрезанное до 300х300px:
ffmpeg -i source.jpg -filter:v "scale=300:-1,crop=300:300:0:0" -y crop300x300.jpg

Таким образом мы получили первый thumb:

Все замечательно работает локально, но в лямбде нет ffmpeg. Поэтому нужен бинарник для лямбды, который положим в лямбда слой. Чтобы запустить консольную команду из JavaScript воспользуемся child_process:
const childProcess = require('child_process') spawnPromise(command, argsarray, envOptions) { return new Promise((resolve, reject) => { const childProc = childProcess.spawn(command, argsarray, envOptions || { env: process.env, cwd: process.cwd() }), resultBuffers = [] childProc.stdout.on('data', buffer => { resultBuffers.push(buffer) }) childProc.stderr.on('data', buffer => console.log(buffer.toString())) childProc.on('exit', (code, signal) => { if (code || signal) { reject(`${command} failed with ${code || signal}`) } else { resolve(Buffer.concat(resultBuffers).toString().trim()) } }) }) }
Также нам надо будет взять мета информацию из картинки: ширину, высоту, угол поворота (необходим, для правильного поворота картинки если делать снимок на телефон). Для этого воспользуемся ffprobe. Также, разместим бинарник в лямбда слое. И сделаем функцию, которая будет доставать эти данные:
async getResolution(inputFile) { try { let rotate = await this.spawnPromise( config.FFPROBE, [ '-v', 'error', '-select_streams', 'v:0', '-show_entries', 'stream_tags=rotate', '-of', 'default=nw=1:nk=1', inputFile], ) rotate = rotate || 0 const str = await this.spawnPromise( config.FFPROBE, [ '-v', 'error', '-select_streams', 'v:0', '-show_entries', 'stream=width,height', '-of', 'csv=s=x:p=0', inputFile], ) let [width, height] = String(str).split('x') if (String(rotate) === '90' || String(rotate) === '-90' || String(rotate) === '270' || String(rotate) === '-270') { [width, height] = [height, width] } return { width, height, rotate } } catch (e) { console.log('ffprobe error', e) return { width: 1080, height: 1920 } } }
Теперь, зная мета информацию о файле, можно сделать основную функцию конвертации:
async thumb(inputFile, outputFile, filterData, imageData = {}) { try { const isVertical = Number(imageData.width) < Number(imageData.height) let filter if (filterData.crop) { const newImageWidth = isVertical ? filterData.height : filterData.width const newImageHeight = isVertical ? filterData.width : filterData.height const resizeK = newImageWidth / imageData.width const x = parseInt(isVertical ? 0 : (resizeK * imageData.width / 2 - newImageWidth / 2)) const y = parseInt(isVertical ? (resizeK * imageData.height / 2 - newImageHeight / 2) : 0) if (newImageWidth === newImageHeight) { // square filter = `scale=${isVertical ? `${newImageWidth}:-1` : `-1:${newImageWidth}`},crop=${newImageWidth}:${newImageHeight}:${x}:${y}` } else { filter = `scale=${isVertical ? `${newImageWidth}:-1` : `-1:${newImageWidth}`},crop=${newImageWidth}:${newImageHeight}:${x}:${y}` } } else { const newImageWidth = isVertical ? filterData.height : filterData.width filter = `scale=${newImageWidth}:-1` } if (Number(imageData.rotate) !== 0) { const times = Number(imageData.rotate) / 90 for (let i = 0; i < times; i++) { filter += `,transpose=1` } } const callData = [ '-v', 'error', '-noautorotate', '-i', inputFile, '-filter:v', filter, '-y', ] return await this.spawnPromise( config.FFMPEG, [ ...callData, outputFile], ) } catch (e) { console.log('ffmpeg error', e) }
Я это делал для одного из своих проектов и первая проблема, с которой столкнулись пользователи, это загрузка картинок из iPhone. Так как Apple сделали свой формат изображений, который еще не все поддерживают. Пока единственное решение, которое я нашел это скомпилировать tifig бинарник и добавить его в слой для лямбды. К сожалению, все бинарники я компилировал для лямбды давно и о том, как скомпилировать tifig для лямбды не расскажу, но зато поделюсь готовыми бинарниками.
И добавим функцию конвертации heic в jpg:
heicToJpg(filePath, newFilePath) { return new Promise((resolve, reject) => { childProcess.exec(se([config.TIFIG, filePath, newFilePath]), { encoding: 'utf8' }, function (error, stdout, stderr) { if (error) { console.error(stderr) return reject(error) } fs.unlinkSync(filePath) return resolve(true) }) }) }
Итак, основной код лямбды на самом высоком уровне выглядит вот так:
module.exports.handler = async (event) => { const s3Record = event.Records[0].s3 const sourceKey = s3Record.object.key if (!file.isPhoto(sourceKey)) { return true } await file.download(s3Record.bucket.name, sourceKey,file.getDownloadName(sourceKey)) const ext = file.getExt(sourceKey) const isHeic = file.isHeic(sourceKey) // convert iphone photo format to jpg if (isHeic) { await convert.heicToJpg(file.getDownloadName(sourceKey), file.getDownloadNameIfHeic(sourceKey, ext)) } // create thumbs by config.RESOLUTIONS const { width, height, rotate } = await convert.getResolution(file.getDownloadNameIfHeic(sourceKey, ext)) await Promise.all(config.RESOLUTIONS.map(r => createThumbAndUpload(s3Record, r, { width, height, rotate, ext }))) // remove unnecessary files await file.remove(file.getDownloadName(sourceKey)) if (isHeic) { await file.remove(file.getDownloadNameIfHeic(sourceKey, ext)) } return true }
При этом всем config.RESOLUTIONS это константа настройки, при помощи которой можно задать в какие форматы необходимо конвертировать изображения. Для примера я сделал несколько вариантов:
RESOLUTIONS: [ { name: '1920x1080', width: 1920, height: 1080 }, { name: '1280x720', width: 1280, height: 720 }, { name: 'crop600x400', crop: true, width: 600, height: 400 }, { name: 'crop300x300', crop: true, width: 300, height: 300 }, ]
Развернем все в облаке. Итак подготовим конфиг деплоя Serverless Framework с поясняющими коментариями:
service: service-lambda-thumb-tifig frameworkVersion: '2 || 3' app: lambda-thumb-tifig package: # исключаем лишние файлы из архива лямбды, чтоб уменьшить ее размер patterns: - '!node_modules/aws-sdk' - '!node_modules/@aws-cdk' - '!node_modules/serverless' - '!node_modules/serverless-lift' - '!.idea' - '!yarn.lock' - '!yarn.error.log' - '!README.md' - '!.gitignore' - '!package.json' - '!lib' - '!.git' custom: name: 'lambda-thumb-tifig' environment: 'prod' region: 'us-east-1' lambda_prefix: ${self:custom.environment}-${self:custom.name} # Отмечу что я использовал авто создание S3 для демо. # Но в реальности можно подключить уже раннее созданные S3. # этот конфиг создает новые S3 constructs: sourceBucket: type: storage destinationBucket: type: storage provider: name: aws lambdaHashingVersion: '20201221' environment: DESTINATION_BUCKET: ${construct:destinationBucket.bucketName} REGION: ${self:custom.region} region: ${self:custom.region} runtime: nodejs14.x iamRoleStatements: - Effect: "Allow" Action: - "s3:PutBucketNotification" - "s3:GetObject" - "s3:PutObject" Resource: Fn::Join: - "" - - "arn:aws:s3:::*" - Effect: "Allow" Action: - "rds:*" Resource: "*" plugins: - serverless-lift functions: # создаем лямбда-функцию ffmpeg-tifig: # Увеличиваем память и процессор лямбда функции # для увеличения быстродействия # такие лямбды будут дороже, но так как в этом проекте # я не выхожу за бесплатные лимиты в месяц - буду использовать # самый быстрый вариант memorySize: 10240 name: ${self:custom.lambda_prefix} handler: src/index.handler # Увеличиваем максимальное время выполнения функции timeout: 300 # Подключим функцию к собитию загрузки файла в S3 Source events: - s3: bucket: ${construct:sourceBucket.bucketName} existing: true # Подключим слой Lib к лямбда-функции layers: - { Ref: LibLambdaLayer } # Создадим лямбда слой и поместим в него все содержимое папки lib. layers: lib: path: lib
Развернем это все в AWS:
sls deploy
В результате получаем лямбда функцию, подключенную к S3:

Проверим это все в AWS Console, загрузив несколько картинок в Source S3. И посмотрим в Destination S3:

Весь код доступен тут. Если у вас возникло желание каким-либо образом улучшить код — прошу сделать PR в этот репозиторий. Сделаем мир лучше вместе.
ссылка на оригинал статьи https://habr.com/ru/post/648519/
Добавить комментарий