Серверный инструмент обработки изображений Node.js — расширенное руководство по работе с Sharp

Node.js задняя часть внешний интерфейс SVG

sharpЭто очень популярная библиотека обработки изображений на платформе Node.js, которая фактически основана на языке C.libvipsБиблиотека упакована, поэтому высокая производительность стала важным преимуществом Sharp. Sharp может легко реализовать стандартные операции редактирования изображений, такие как кадрирование, преобразование формата, преобразование поворота, добавление фильтров и т. д. Конечно, в Интернете есть много статей на эту тему.официальная документацияЭто также более подробно, так что это не является предметом этой статьи. Здесь я в основном хочу записать некоторые решения для некоторых немного сложных требований к обработке изображений, с которыми я столкнулся в процессе использования Sharp, и я надеюсь, что обмен ими может быть полезен для всех.

острые основы

Sharp применяет режим потоковой передачи в целом, который проходит серию операций обработки после считывания данных изображения, а затем выводит результат. Мы можем понять это, взглянув на простой пример:

const sharp = require('sharp');
sharp('input.jpg')
  .rotate()
  .resize(200)
  .toBuffer()
  .then( data => ... )
  .catch( err => ... );

Почти все функциональные интерфейсы Sharp смонтированы вSharpНапример, первой операцией обработки изображения должно быть чтение данных изображения (sharpФункция принимает локальный путь изображения или данные буфера изображения в качестве параметра) и преобразует его вSharpэкземпляр, а затем обрабатывается как сборочная линия. Поэтому здесь должна быть предусмотрена функция предварительной обработки для преобразования изображения, полученного сервером, вSharpПример:

/**
*
* @param  { String | Buffer } inputImg 图片本地路径或图片 Buffer 数据
* @return { Sharp }
*/
async convert2Sharp(inputImg) {
    return sharp(inputImg)
}

Затем можно выполнить конкретную обработку изображения.

Добавить водный знак

бэкэнд реализация

Добавление функции водяного знака следует рассматривать как относительно распространенное требование к обработке изображений. Sharp предоставляет только одну функцию для синтеза изображения:overlayWith, который принимает параметр изображения (а также строку локального пути к изображению или данные буфера изображения) и необязательныйoptionsНастройте объект (вы можете настроить расположение изображения водяного знака и другую информацию), а затем наложите изображение на исходное изображение. Логика также относительно проста, наш код выглядит следующим образом:

/**
* 添加水印
* @param  { Sharp  } img 原图
* @param  { String } watermarkRaw 水印图片
* @param  { top } 水印距图片上边缘距离
* @param  { left } 水印距图片左边缘距离
*/
async watermark(img, { watermarkRaw, top, left }) {
    const watermarkImg = await watermarkRaw.toBuffer()
    return img
        .overlayWith(watermarkImg, { top, left })
}

Для простоты поддерживается только расположение изображения водяного знака.Sharp также поддерживает более сложные параметры конфигурации, например, следует ли повторно вставлять несколько изображений водяных знаков, вставлять ли изображение водяного знака только в канал α и т. д. Подробнее , пожалуйста, обратитесь кoverlayWithдокументация.

Интерфейсная реализация

Здесь, кстати, тоже нужно упомянуть о реализации фронтенда. Конечно, если сервер добавляет водяной знак к изображению в соответствии с фиксированными правилами (например, водяной знак изображения размещается в фиксированной позиции в Sina Weibo), интерфейсу ничего не нужно делать. Однако в некоторых сценариях (например, в онлайн-инструментах для редактирования изображений) пользователи ожидают, что при добавлении водяных знаков во внешнем интерфейсе будет работать WYSIWYG. В это время, если пользователь добавит водяной знак и выберет локацию, данные должны быть отправлены на сервер для обработки и тогда будет получен результат обработки, что неминуемо повлияет на плавность работы всего сервиса. К счастью, мощный HTML5 делает внешний интерфейс все более и более богатым с помощьюcanvasМы можем реализовать функцию добавления водяных знаков в интерфейсе. Конкретные детали реализации не сложны, главное использоватьcanvasкоторый предоставилdrawImageметод, посмотрите на примере:

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');

// img: 底图
// watermarkImg: 水印图片
// x, y 是画布上放置 img 的坐标
ctx.drawImage(img, x, y);
ctx.drawImage(watermarkImg, x, y);

Фактически вся функция добавления водяных знаков (выбор исходного изображения, выбор изображения водяного знака, установка положения изображения водяного знака и получение изображения водяного знака) может быть полностью выполнена внешним интерфейсом. Конечно, чтобы обеспечить целостность функций на стороне сервера, рекомендуется использовать режим внешнего отображения + внутренней обработки.

вставить текст

Вставьте текст и добавьте водяные знаки, которые на самом деле относительно похожи. Единственная разница заключается в добавлении изображения водяного знака в текст, и нам может понадобиться размер текста, шрифты и т. д., чтобы внести некоторые коррективы. Идея относительно проста для мышления, преобразовать текст в форму изображения можно. Здесь мы используемtext-to-svgБиблиотека, которая конвертирует текст в svg. Используя характеристики svg, мы можем легко установить размер шрифта, цвет и т. д. текста. тогда позвониBuffer.fromПреобразуйте svg в буфер данных, который может использовать Sharp. Последний шаг — добавить тот же водяной знак, что и выше.

const Text2SVG = require('text-to-svg')

/**
* 粘贴文字
* @param  { Sharp  } img
* @param  { String } text 待粘贴文字
* @param  { Number } fontSize 文字大小
* @param  { String } color 文字颜色
* @param  { Number } left 文字距图片左边缘距离
* @param  { Number } top 文字距图片上边缘距离
*/
async pasteText(img, {
    text, fontSize, color, left, top,
}) {
    const text2SVG = Text2SVG.loadSync()
    const attributes = { fill: color }
    const options = {
        fontSize,
        anchor: 'top',
        attributes,
    }
    const svg = Buffer.from(text2SVG.getSVG(text, options))
    return img
        .overlayWith(svg, { left, top })
}

Сшивание картинок

Операция сшивания картинок относительно самая сложная. Здесь мы предоставляем два элемента конфигурации: режим сшивания (горизонтальный/вертикальный) и цвет фона. Режим сшивания проще для понимания, и это не что иное, как расположение картинок по горизонтали или по вертикали. Цвет фона используется для заполнения пустого пространства. При сшивке картинки центрируются по оси. Возьмем для примера горизонтальное расположение картинок, принципиальная схема выглядит следующим образом:

Здесь нет готовой функции, предоставляемой шарпом, и все по-прежнему уникально.overlayWithрешить.overlayWithИспользование состоит в том, чтобы вставить одно изображение в другое изображение, что немного отличается от нашей необходимости сшивания изображений. Нам нужно изменить свое мышление: мы можем заранее создать базовую карту, цвет фона можно определить в соответствии со значением конфигурации, а затем наклеить на нее все изображения для сшивания в соответствии с требованиями.

Сначала нам нужно прочитать длину и ширину всех изображений, которые нужно сшить. Предполагая, что режим сращивания — горизонтальное сращивание, ширина конечного сгенерированного изображения представляет собой сумму ширин всех изображений, а высота — это максимальная высота всех изображений (обратное значение для вертикального сращивания):

let totalWidth = 0
let totalHeight = 0
let maxWidth = 0
let maxHeight = 0
const imgMetadataList = []
// 获取所有图片的宽和高,计算和及最大值
for (let i = 0, j = imgList.length; i < j; i += i) {
    const { width, height } = await imgList[i].metadata()
    imgMetadataList.push({ width, height })
    totalHeight += height
    totalWidth += width
    maxHeight = Math.max(maxHeight, height)
    maxWidth = Math.max(maxWidth, width)
}

Затем мы используем полученные данные ширины и высоты для создания нового базового изображения с фоновым цветом входящей конфигурации (или белым по умолчанию):

const baseOpt = {
    width: mode === 'horizontal' ? totalWidth : maxWidth,
    height: mode === 'vertical' ? totalHeight : maxHeight,
    channels: 4,
    background: background || {
        r: 255, g: 255, b: 255, alpha: 1,
    },
}

const base = sharp({
    create: baseOpt,
}).jpeg().toBuffer()

Затем повторно вызывайте на основе базового изображенияoverlayWithФункция для вставки изображений, подлежащих сращиванию, в базовое изображение по одному. Здесь необходимо обратить внимание на размещение изображения.Как упоминалось ранее, мы будем центрировать изображение в соответствии с главной осью, поэтому каждый раз, когда изображение размещается, необходимо выполнять верхний и левый расчеты (один расчет центрирования, один из них - расчет смещения с порядком изображений), конечно, после того, как вы поймете принцип, это математическая задача начальной школы, и здесь не о чем говорить. Еще одна вещь, которую следует отметить, это то, чтоoverlayWithЗа один раз можно составить только композицию между двумя картинками, поэтому мы используемreduceметод, который непрерывно вставляет изображение на базовую карту и использует результат в качестве входных данных в следующий раз.

imgMetadataList.unshift({ width: 0, height: 0 })
let imgIndex = 0
const result = await imgList.reduce(async (input, overlay) => {
    const offsetOpt = {}
    if (mode === 'horizontal') {
        offsetOpt.left = imgMetadataList[imgIndex++].width
        offsetOpt.top = (maxHeight - imgMetadataList[imgIndex].height) / 2
    } else {
        offsetOpt.top = imgMetadataList[imgIndex++].height
        offsetOpt.left = (maxWidth - imgMetadataList[imgIndex].width) / 2
    }
    overlay = await overlay.toBuffer()
    return input.then(data => sharp(data).overlayWith(overlay, offsetOpt).jpeg().toBuffer())
}, base)
return result

Ниже приведена полная реализация функции сшивания изображения:

/**
* 拼接图片
* @param  { Array<Sharp> } imgList
* @param  { String } mode 拼接模式:horizontal(水平)/vertical(垂直)
* @param  { Object } background 背景颜色 格式为 {r: 0-255, g: 0-255, b: 0-255, alpha: 0-1} 默认 {r: 255, g: 255, b: 255, alpha: 1}
*/
async joinImage(imgList, { mode, background }) {
    let totalWidth = 0
    let totalHeight = 0
    let maxWidth = 0
    let maxHeight = 0
    const imgMetadataList = []
    // 获取所有图片的宽和高,计算和及最大值
    for (let i = 0, j = imgList.length; i < j; i += i) {
        const { width, height } = await imgList[i].metadata()
        imgMetadataList.push({ width, height })
        totalHeight += height
        totalWidth += width
        maxHeight = Math.max(maxHeight, height)
        maxWidth = Math.max(maxWidth, width)
    }

    const baseOpt = {
        width: mode === 'horizontal' ? totalWidth : maxWidth,
        height: mode === 'vertical' ? totalHeight : maxHeight,
        channels: 4,
        background: background || {
            r: 255, g: 255, b: 255, alpha: 1,
        },
    }

    const base = sharp({
        create: baseOpt,
    }).jpeg().toBuffer()

    // 获取图片的原始尺寸用于偏移
    imgMetadataList.unshift({ width: 0, height: 0 })
    let imgIndex = 0
    const result = await imgList.reduce(async (input, overlay) => {
        const offsetOpt = {}
        if (mode === 'horizontal') {
            offsetOpt.left = imgMetadataList[imgIndex++].width
            offsetOpt.top = (maxHeight - imgMetadataList[imgIndex].height) / 2
        } else {
            offsetOpt.top = imgMetadataList[imgIndex++].height
            offsetOpt.left = (maxWidth - imgMetadataList[imgIndex].width) / 2
        }
        overlay = await overlay.toBuffer()
        return input.then(data => sharp(data).overlayWith(overlay, offsetOpt).jpeg().toBuffer())
    }, base)
    return result
},

Выше приведены некоторые практические операции, обобщенные отдельными лицами в процессе использования шара. На самом деле, есть много расширенных функций шарпа, которыми я не пользовался, что и является «законом 28»: 80% требований часто выполняются на 20% функций. Если в будущем все еще будет шанс использовать более острое, я продолжу делиться им с вами ~

Эта статья была впервые опубликована в моем блоге (Нажмите здесь, чтобы просмотреть), добро пожаловать, чтобы следовать.