跳至主要内容
Deno 2 终于发布了 🎉️
了解更多
Resizing an image with Deno

用不到 100 行代码构建一个简单的图像调整大小 API

在本教程中,我们将使用 ImageMagick 构建一个简单的图像调整大小 API,并在 Deno Deploy 上部署它。该 API 将通过拉伸或裁剪来调整图像大小,尽管您以后可以添加更多功能。

Follow the video tutorial for building an image resizing API.

请观看此处的视频教程。

查看源代码试用演示,或 查看游乐场

ImageMagick

我们将使用 ImageMagick 来完成 API 中的所有图像操作。它是用于操作图像的最流行的库之一,包括调整大小、格式转换、压缩和效果应用。更具体地说,我们将使用 imagemagick_deno,它将 ImageMagick 编译为 WebAssembly,并且与 Deno 和 Deno Deploy 兼容。

设置服务器

首先,为您的项目创建一个目录。然后,在该文件夹中创建一个名为 main.ts 的新 TypeScript 文件。

main.ts 中,让我们使用 std lib 的 http 模块 创建一个基本的 Web 服务器。

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 的新函数,它将

  • 检查必要的参数 imageheightwidth
  • 确保 heightwidth 大于 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 的新函数,它将通过 URL fetch 图像,检查 mediaType 是否正确,并返回一个 buffermediaType(或错误消息)。注意,我们需要在文件顶部导入 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 的新函数,它将接受 imageBufferparams 对象。

在此函数中,我们将使用 MagickGeometry 构造函数来设置调整大小的参数。此外,为了允许自适应调整大小,如果 heightwidth 缺失,我们将 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 应该会显示以下内容

Squished resized image example

您有一个可以工作的图像调整器!

部署到 Deno Deploy

您可以使用 Deno Deploy(我们的多租户 JavaScript 无服务器云)在边缘托管您的应用程序。

首先,创建一个包含 main.ts 的 GitHub 仓库。是的,它可以是一个单文件仓库。然后,访问 https://deno.org.cn/deploy 并连接您的 GitHub 帐户。从 GitHub 创建一个新项目,并选择您刚刚创建的仓库。选择“GitHub 自动”,这将每次合并到您的 main 分支时都进行部署。最后,您的入口点应该是 main.ts

连接后,它应该需要最多一分钟才能部署。部署完成后,您可以访问您的 URL。

下一步是什么?

使用 Deno 和 ImageMagick 构建一个图像调整大小 API 不仅简单快捷,而且可以在 100 行代码内完成。不仅如此,您还可以将此 API 全局托管在边缘,以最大程度地减少 API 消费者的延迟。您可以通过添加文本或将其保存到存储服务等功能来扩展此 API。

遇到问题或想分享您的工作成果?快来 DiscordTwitter 上与我们聊天吧!