Создание изображений в runtime (favicon, watermark, нарезка картинок) #golang

от автора

В Go есть возможность создавать файлы изображений.

С помощью этого мы можем создавать картинки на лету (в runtime).

Где же это может пригодится?

Вот небольшой список того, что мы можем создать используя данный функционал:

  1. Favicon

  2. Pixel-трекер

  3. Placeholder

  4. Наложение текста (watermark) на изображение

  5. Нарезка изображений

1. Favicon

Часто Go приложения рассматриваются как серверная часть для отдачи контента для внутренних и/или внешних сервисов и  может отдавать контент для Вашего сайта.

Желательно, чтобы у Вас был внешний сервис (слой) для отдачи ответа — некий getaway, чтобы отдавать только нужный payload и, в том числе, без веб-специфичных запросов.

Под веб-специфичными запросами от клиента к серверной части — я подразумеваю запросы favicon и различные манифесты, которые принято рассматривать в большей степени как «фронтовые» типы ресурсов. То есть за их работу в большинстве случаев отвечают те же специалисты, которые и делают «фронтовую» часть проекта.

Но, что если ваше Go приложение также сможет отдавать favicon и манифесты самостоятельно. И не просто брать статичные файлы favicon и манифеста, а генерить/создавать эти массивы байт в runtime. Это позволит нашему приложению не зависеть от этих статичных файлов.

Вы, конечно, можете отдавать статичный файл:

func faviconHandler(w http.ResponseWriter, r *http.Request) { 	http.ServeFile(w, r, "relative/path/to/favicon.ico") } ... func main() {   http.HandleFunc("/favicon.ico", faviconHandler) }

Но мы же хотим создавать favicon динамически.

Для динамического создания favicon есть, как минимум, два похожих способа:

  • Первый способ

import (    "bytes"    "image"    "image/gif"    "net/http"    ... )  var buf bytes.Buffer gif.Encode(&buf, image.Rect(0, 0, 16, 16), nil)  w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Content-Length", strconv.Itoa(len(buf.Bytes()))) w.Write(buf.Bytes())

Второй способ:

import (    "bytes"    "image"    "image/color"    "image/draw"    "image/jpeg"    ... )  buf := new(bytes.Buffer)  m := image.NewRGBA(image.Rect(0, 0, 16, 16)) clr := color.RGBA{B: 0, A: 0} draw.Draw(m, m.Bounds(), image.NewUniform(clr), image.Pointer{}, draw.Src) jpeg.Encode(buffer, img, nil)  w.Header().Set("Content-Type", "image/jpeg") w.Header().Set("Content-Length", strconv.Itoa(len(buffer.Bytes()))) w.Write(buf.Bytes())

2. Pixel-трекер

Pixel-трекер широко используется при арбитраже трафика.

var buf bytes.Buffer gif.Encode(&buf, image.Rect(0, 0, 1, 1, nil))

Динамическая генерация такого трекера позволит Вам в одном приложении отдавать и pixel, и собирать данные.

Для генерации placeholder-ов мы будем использовать тот же подход, что и для картинок с фиксированным размером, но с возможностью добавлять текст и изменять размеры картинки и шрифта.

Во-первых, определимся с требованиями:

  • на вход к нам приходим запрос вида: /ШИРИНА/ВЫСОТА/ЦВЕТ/ТЕКСТ/ЦВЕТ_ТЕКСТА/РАЗМЕР_ШРИФТА

    Например: /600/200/622E68/Placeholder (заглушка)/FFFFFF

  • текст должен располагаться по середине картинки

  • текст приходит в запросе от клиента или собирается по формату: «ШИРИНА х ВЫСОТА»

  • текст должен поддерживать возможность латиницы и кириллицы

  • предусмотреть параметры по умолчанию

  • на выходе мы отдаём байты картинки

Разбор параметров начнём с пакета по работе с цветами картинки и текста (package colors).

Цвет будет приходить как строка в HEX формате. Другие форматы цвета, как входного параметра — не предвидятся.

Пакет для работы с цветами:

package colors  import ( 	"image/color" 	"strconv" )  type rgb struct { 	red   uint8 	green uint8 	blue  uint8 }  func ToRGBA(h string) (color.RGBA, error) { 	rgb, err := hex2RGB(h) 	if err != nil { 		return color.RGBA{}, err 	}  	return color.RGBA{R: rgb.red, G: rgb.green, B: rgb.blue, A: 255}, nil }  func hex2RGB(hex string) (rgb, error) { 	values, err := strconv.ParseUint(hex, 16, 32) 	if err != nil { 		return rgb{}, err 	}  	return rgb{ 		red:   uint8(values >> 16), 		green: uint8((values >> 8) & 0xFF), 		blue:  uint8(values & 0xFF), 	}, nil }

Непосредственно пакет сборки нашей картинки:

pacakge img  const (         // Параметры по умолчанию 	imgColorDefault = "E5E5E5" 	msgColorDefault = "AAAAAA" 	imgWDefault     = 300 	imgHDefault     = 300 	fontSizeDefault         = 0 	dpiDefault      float64 = 72  	fontfileDefault = "wqy-zenhei.ttf" )  // Do - входная точка. func Do(params []string) (*bytes.Buffer, error) {         // fetch img params: imgW, imgH, text, etc          // Соберём структуру Текста 	label := Label{Text: msg, FontSize: fontSize, Color: msgColor} 	// Соберём структуру Картинки с нужными полями - высота, ширина, цвет и текст 	img := Img{Width: imgW, Height: imgH, Color: imgColor, Label: label}  	// Сгенерим нашу картинку с текстом 	return img.generate() }  // generate - соберёт картинку по нужным размерам, цветом и текстом. func (i Img) generate() (*bytes.Buffer, error) { 	// Если есть размеры и нет требований по Тексту - соберём Текст по умолчанию. 	if ((i.Width > 0 || i.Height > 0) && i.Text == "") || i.Text == "" { 		i.Text = fmt.Sprintf("%d x %d", i.Width, i.Height) 	} 	// Если нет требований по размеру шрифта - подберём его исходя из размеров картинки. 	if i.FontSize == 0 { 		i.FontSize = i.Width / 10 		if i.Height < i.Width { 			i.FontSize = i.Height / 5 		} 	} 	// Переведём цвет из строки в color.RGBA. 	clr, err := colors.ToRGBA(i.Color) 	if err != nil { 		return nil, err 	} 	// Создадим in-memory картинку с нужными размерами. 	m := image.NewRGBA(image.Rect(0, 0, i.Width, i.Height)) 	// Отрисуем картинку: 	// - по размерам (Bounds) 	// - и с цветом (Uniform - обёртка над color.Color c Image функциями) 	// - исходя из точки (Point), как базовой картинки 	// - заполним цветом нашу Uniform (draw.Src) 	draw.Draw(m, m.Bounds(), image.NewUniform(clr), image.Point{}, draw.Src) 	// Добавим текст в картинку. 	if err = i.drawLabel(m); err != nil { 		return nil, err 	} 	var im image.Image = m 	// Выделим память под нашу данные (байты картинки). 	buffer := &bytes.Buffer{} 	// Закодируем картинку в нашу аллоцированную память. 	err = jpeg.Encode(buffer, im, nil)  	return buffer, err }  // drawLabel - добавит текст на картинку. func (i *Img) drawLabel(m *image.RGBA) error { 	// Разберём цвет текста из строки в RGBA. 	clr, err := colors.ToRGBA(i.Label.Color) 	if err != nil { 		return err 	} 	// Получим шрифт (должен работать и с латиницей и с кириллицей). 	fontBytes, err := ioutil.ReadFile(fontfileDefault) 	if err != nil { 		return err 	} 	fnt, err := truetype.Parse(fontBytes) 	if err != nil { 		return err 	} 	// Подготовим Drawer для отрисовки текста на картинке. 	d := &font.Drawer{ 		Dst: m, 		Src: image.NewUniform(clr), 		Face: truetype.NewFace(fnt, &truetype.Options{ 			Size:    float64(i.FontSize), 			DPI:     dpiDefault, 			Hinting: font.HintingNone, 		}), 	} 	// Зададим базовую линию. 	d.Dot = fixed.Point26_6{ 		X: (fixed.I(i.Width) - d.MeasureString(i.Text)) / 2, 		Y: fixed.I((i.Height+i.FontSize)/2 - 12), 	} 	// Непосредственно отрисовка текста в нашу RGBA картинку. 	d.DrawString(i.Text)  	return nil }

Пример, на запрос:

http://localhost:8080/600/200/004620/Заглушка/FFFFFF/50

получим:

Обратите внимание: мы можем создавать изображение с текстом на латинице и/или на кириллице.

А на такой запрос:

http://localhost:8080/480/200

4. Наложение текста (watermark) на изображение

Watermark может пригодится в таких кейсах:

  • для указания правообладателя на публикуемом изображении

  • на изображение можно накладывать текст рекламного характера

  • назначение изображения и его ограничения для использования

Например, указать, что данное изображение или фото могут быть использованы только для определённых целей и/или в ограниченные сроки.

5. Нарезка изображений

Не редко на платформе есть необходимость хранить набор изображений и их нарезки, по размерам и форматам.

В в какой-то момент может прийти требование изменить набор нарезанных размеров.

В этом случае необходимо производить нарезку новых размеров и, возможно, удалить другие размеры.

Возможны два сценария реализации нарезки:

  • разовая (нарезка для всего набора по требованию)

  • динамическая (нарезка в runtime)

У разовой нарезки есть неоспоримое преимущество — мы разово (например, раз в сутки) нарезаем все картинки или часть из них.

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

Динамическая нарезка изображений, позволяет освободить место под все «нарезки» и не беспокоиться об их актуальности.

Но в данном случае мы требовательны к вычислительным ресурсам и слой «кеширования» нарезки можно «сдвинуть» ближе к клиенту и в любой момент обновить набор размеров.

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

Также если у Вас часто меняются размеры и формат изображений — Вам скорее больше подходит динамическая нарезка (при наличии вычислительных мощностей). Настоятельно рекомендую делать просчёты под Ваши планируемые требования и мощности.

Примечание

Все примеры кода приведены в ознакомительных целях. Соответственно, Вам нужно проверять и адаптировать данный подход под Ваши требования и цели.

ссылка на оригинал статьи https://habr.com/ru/post/530134/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *