Первоначально опубликовано на deno.com/blog.

В этом руководстве мы создадим простой API для изменения размера изображения с помощью ImageMagick и развернем его на Deno Deploy. API будет изменять размер изображений, растягивая или обрезая их, хотя позже вы сможете добавить дополнительные функции.

Просмотреть исходный код, попробовать демоверсию или заценить игровую площадку.

ImageMagick

Мы будем использовать ImageMagick для всех операций с изображениями в нашем API. Это одна из самых популярных библиотек для управления изображениями, включая изменение размера, преобразование формата, сжатие и применение эффектов. В частности, мы будем использовать imagemagick_deno, который ImageMagick скомпилирован в WebAssembly и совместим с Deno и Deno Deploy.

Настройка сервера

Сначала создайте каталог для вашего проекта. Затем создайте новый файл TypeScript в этой папке с именем main.ts.

В main.ts давайте создадим базовый веб-сервер, используя модуль http из стандартной библиотеки.

import { serve } from "https://deno.land/[email protected]/http/server.ts";
serve(
  async (req: Request) => {
    return new Response("Hello World!");
  },
);

Теперь вы можете запустить этот скрипт, открыв терминал в этом каталоге и выполнив:

deno run --allow-net main.ts

Теперь перейдите к localhost:8000. Вы должны увидеть Hello World!.

Управление изображениями

Следующим шагом является добавление обработки изображений. В main.ts давайте импортируем несколько вещей из imagemagick_deno:

import {
  ImageMagick,
  initializeImageMagick,
  MagickGeometry,
} from "https://deno.land/x/[email protected]/mod.ts";

Затем давайте инициализируем ImageMagick, который устанавливает необходимую конфигурацию для работы бинарника и его API.

await initializeImageMagick();

Затем мы подтвердим, что необходимые параметры существуют и имеют смысл, или вернем код ошибки 400 (неверный запрос). Мы можем получить доступ к параметрам строки запроса с помощью функции searchParams.

Давайте создадим новую функцию parseParams, которая будет:

  • проверьте необходимые параметры image, height и width
  • убедитесь, что height и width больше 0 и меньше 2048
  • вернуть string при ошибке
function parseParams(reqUrl: URL) {
  const image = reqUrl.searchParams.get("image");
  if (image == null) {
    return "Missing 'image' query parameter.";
  }
  const height = Number(reqUrl.searchParams.get("height")) || 0;
  const width = Number(reqUrl.searchParams.get("width")) || 0;
  if (height === 0 && width === 0) {
    return "Missing non-zero 'height' or 'width' query parameter.";
  }
  if (height < 0 || width < 0) {
    return "Negative height or width is not supported.";
  }
  const maxDimension = 2048;
  if (height > maxDimension || width > maxDimension) {
    return `Width and height cannot exceed ${maxDimension}.`;
  }
  return {
    image,
    height,
    width,
  };
}

В нашей функции serve() мы можем получить доступ к параметрам строки запроса и вызвать parseParams:

// ...
serve(
  async (req) => {
    const reqURL = new URL(req.url);
    const params = parseParams(reqURL);
    if (typeof params === "string") {
      return new Response(params, { status: 400 });
    }
  },
);

Теперь, когда мы убедились в наличии необходимых параметров, давайте получим изображение. Давайте создадим новую функцию getRemoteImage, которая будет fetch отображать изображение по URL-адресу, проверять правильность mediaType и возвращать buffer и mediaType (или сообщение об ошибке). Обратите внимание, нам нужно будет импортировать parseMediaType вверху файла.

import { parseMediaType } from "https://deno.land/[email protected]/media_types/parse_media_type.ts";
async function getRemoteImage(image: string) {
  const sourceRes = await fetch(image);
  if (!sourceRes.ok) {
    return "Error retrieving image from URL.";
  }
  const mediaType = parseMediaType(sourceRes.headers.get("Content-Type")!)[0];
  if (mediaType.split("/")[0] !== "image") {
    return "URL is not image type.";
  }
  return {
    buffer: new Uint8Array(await sourceRes.arrayBuffer()),
    mediaType,
  };
}

Затем в нашей функции serve() после parseParams мы можем вызвать getRemoteImage и обработать любые ошибки:

// ...
serve(
  async (req) => {
    const reqURL = new URL(req.url);
    const params = parseParams(reqURL);
    if (typeof params === "string") {
      return new Response(params, { status: 400 });
    }
    const remoteImage = await getRemoteImage(params.image);
    if (remoteImage === "string") {
      return new Response(remoteImage, { status: 400 });
    }
  },
);

Затем мы можем, наконец, изменить изображение с помощью ImageMagick. Давайте создадим новую функцию с именем modifyImage, которая примет imageBuffer и объект params.

В этой функции мы будем использовать конструктор MagickGeometry для установки параметров изменения размера. Кроме того, чтобы обеспечить адаптивное изменение размера, если отсутствует height или width, мы установим ignoreAspectRatio на false.

Наконец, эта функция вернет Promise, который будет преобразован в преобразованное изображение как буфер Uint8Array:

function modifyImage(
  imageBuffer: Uint8Array,
  params: { width: number; height: number; mode: string },
) {
  const sizingData = new MagickGeometry(
    params.width,
    params.height,
  );
  sizingData.ignoreAspectRatio = params.height > 0 && params.width > 0;
  return new Promise<Uint8Array>((resolve) => {
    ImageMagick.read(imageBuffer, (image) => {
      image.resize(sizingData);
      image.write((data) => resolve(data));
    });
  });
}

В serve() после вызова getRemoteImage добавим вызов modifyImage. Затем мы можем вернуть новый Response, который содержит modifiedImage:

// ...
serve(
  async (req: Request) => {
    const reqURL = new URL(req.url);
    const params = parseParams(reqURL);
    if (typeof params === "string") {
      return new Response(params, { status: 400 });
    }
    const remoteImage = await getRemoteImage(params.image);
    if (remoteImage === "string") {
      return new Response(remoteImage, { status: 400 });
    }
    const modifiedImage = await modifyImage(remoteImage.buffer, params);
    return new Response(modifiedImage, {
      headers: {
        "Content-Type": remoteImage.mediaType,
      },
    });
  },
);

Поздравляем! Вы создали API для изменения размера изображений.

Далее давайте добавим больше гибкости в API и разрешим обрезку.

Обрезка изображений

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

В функции parseParams проверим допустимые режимы ('crop' и 'resize'), а также добавим mode к возвращаемому объекту:

function parseParams(reqUrl: URL) {
  const image = reqUrl.searchParams.get("image");
  if (image == null) {
    return "Missing 'image' query parameter.";
  }
  const height = Number(reqUrl.searchParams.get("height")) || 0;
  const width = Number(reqUrl.searchParams.get("width")) || 0;
  if (height === 0 && width === 0) {
    return "Missing non-zero 'height' or 'width' query parameter.";
  }
  if (height < 0 || width < 0) {
    return "Negative height or width is not supported.";
  }
  const maxDimension = 2048;
  if (height > maxDimension || width > maxDimension) {
    return `Width and height cannot exceed ${maxDimension}.`;
  }
  const mode = reqUrl.searchParams.get("mode") || "resize";
  if (mode !== "resize" && mode !== "crop") {
    return "Mode not accepted: please use 'resize' or 'crop'.";
  }
  return {
    image,
    height,
    width,
    mode,
  };
}

Затем в функции modifyImage, если режим crop, мы будем использовать image.crop():

function modifyImage(
  imageBuffer: Uint8Array,
  params: { width: number; height: number; mode: "resize" | "crop" },
) {
  const sizingData = new MagickGeometry(
    params.width,
    params.height,
  );
  sizingData.ignoreAspectRatio = params.height > 0 && params.width > 0;
  return new Promise<Uint8Array>((resolve) => {
    ImageMagick.read(imageBuffer, (image) => {
      if (params.mode === "resize") {
        image.resize(sizingData);
      } else {
        image.crop(sizingData);
      }
      image.write((data) => resolve(data));
    });
  });
}

Вот оно! Теперь, если вы хотите попробовать свой API, просто запустите deno run --allow-net main.ts, как мы это делали раньше, и получите к нему доступ, используя URL-адрес, содержащий ширину, высоту, URL-адрес изображения и режим. Например, URL-адрес localhost:8000/?image=https://deno.land/images/artwork/deno_city.jpeg&width=500&height=500 должен дать вам что-то вроде этого:

У вас есть работающая программа для изменения размера изображения!

Развертывание в Deno Deploy

Вы можете разместить свое приложение на периферии с помощью Deno Deploy, нашего многопользовательского бессерверного облака JavaScript.

Сначала создайте репозиторий GitHub, содержащий main.ts. Да, это может быть репозиторий с одним файлом. Затем перейдите на https://deno.com/deploy и подключите свою учетную запись GitHub. Создайте новый проект на GitHub и выберите только что созданный репозиторий. Выберите GitHub Automatic, который будет развертываться каждый раз, когда происходит слияние с вашей веткой main. Наконец, ваша точка входа должна быть main.ts.

После подключения развертывание может занять до минуты. Как только он появится, вы сможете посетить свой URL.

Что дальше?

Создание API для изменения размера изображения с помощью Deno и ImageMagick не только просто и быстро, но и может быть выполнено за 100 строк кода. Кроме того, вы можете разместить этот API глобально на периферии, чтобы свести к минимуму задержку для потребителей вашего API. Вы можете расширить это с помощью дополнительных функций, таких как добавление текста или сохранение его в службе хранения.

Застряли или хотите поделиться тем, над чем работаете? Заходите поздороваться в Discord или Twitter!