跳到主要内容
Resizing an image with Deno

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

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

Follow the video tutorial for building an image resizing API.

在此处观看视频教程。

查看源代码尝试演示,或查看 Playground

ImageMagick

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

设置服务器

首先,为您的项目创建一个目录。然后,在该文件夹中创建一个名为 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 在边缘托管您的应用,Deno Deploy 是我们的多租户 JavaScript 无服务器云。

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

连接后,部署最多需要一分钟。一旦上线,您就可以访问您的 URL。

下一步是什么?

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

遇到困难或想分享您正在做的事情?来 DiscordTwitter 上打个招呼吧!