В Go есть возможность создавать файлы изображений.
С помощью этого мы можем создавать картинки на лету (в runtime).
Где же это может пригодится?
Вот небольшой список того, что мы можем создать используя данный функционал:
-
Favicon
-
Pixel-трекер
-
Placeholder
-
Наложение текста (watermark) на изображение
-
Нарезка изображений
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.Buffergif.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.Buffergif.Encode(&buf, image.Rect(0, 0, 1, 1, nil))
Динамическая генерация такого трекера позволит Вам в одном приложении отдавать и pixel, и собирать данные.
Для генерации placeholder-ов мы будем использовать тот же подход, что и для картинок с фиксированным размером, но с возможностью добавлять текст и изменять размеры картинки и шрифта.
Во-первых, определимся с требованиями:
-
на вход к нам приходим запрос вида: /ШИРИНА/ВСОТА/ЦВЕТ/ТЕКСТ/ЦВЕТ_ТЕКСТА/РАЗМЕР_ШРИФТА
Например: /600/200/622E68/Placeholder (заглушка)/FFFFFF
-
текст должен располагаться по середине картинки
-
текст приходит в запросе от клиента или собирается по формату: "ШИРИНА х ВСОТА"
-
текст должен поддерживать возможность латиницы и кириллицы
-
предусмотреть параметры по умолчанию
-
на выходе мы отдаём байты картинки
Разбор параметров начнём с пакета по работе с цветами картинки и текста (package colors).
Цвет будет приходить как строка в HEX формате. Другие форматы цвета, как входного параметра - не предвидятся.
Пакет для работы с цветами:
package colorsimport ("image/color""strconv")type rgb struct {red uint8green uint8blue 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 imgconst ( // Параметры по умолчаниюimgColorDefault = "E5E5E5"msgColorDefault = "AAAAAA"imgWDefault = 300imgHDefault = 300fontSizeDefault = 0dpiDefault float64 = 72fontfileDefault = "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 / 10if 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)
У разовой нарезки есть неоспоримое преимущество - мы разово (например, раз в сутки) нарезаем все картинки или часть из них.
То есть, в данном случае мы требовательны к дисковому пространству. Быстрые диски ближе к клиенту и ближе к горячим данным (изображениям).
Динамическая нарезка изображений, позволяет освободить место под все "нарезки" и не беспокоиться об их актуальности.
Но в данном случае мы требовательны к вычислительным ресурсам и слой "кеширования" нарезки можно "сдвинуть" ближе к клиенту и в любой момент обновить набор размеров.
То есть идеальным вариантом (для ресурсов) будет гибридная нарезка. Когда мы выделяем данные, которые нужно нарезать динамически или разово.
Также если у Вас часто меняются размеры и формат изображений - Вам скорее больше подходит динамическая нарезка (при наличии вычислительных мощностей). Настоятельно рекомендую делать просчёты под Ваши планируемые требования и мощности.
Примечание
Все примеры кода приведены в ознакомительных целях. Соответственно, Вам нужно проверять и адаптировать данный подход под Ваши требования и цели.