跳到主要内容
Deno 2.4 现已发布,带来了 deno bundle、字节/文本导入、稳定化的 OTel 等功能
了解更多
Resizing an image with Deno

在 100 行代码内构建一个简单的图像缩放 API

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

Follow the video tutorial for building an image resizing API.

请在此处观看视频教程。

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

ImageMagick

我们将在 API 中使用 ImageMagick 进行所有图片处理。它是最受欢迎的图片处理库之一,功能包括调整大小、格式转换、压缩和应用效果。具体来说,我们将使用 imagemagick_deno,它是编译为 WebAssembly 并兼容 Deno 和 Deno Deploy 的 ImageMagick 版本。

设置服务器

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

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

import { serve } from "https://deno.land/std@0.173.0/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/imagemagick_deno@0.0.14/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/std@0.175.0/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

您可以使用我们的多租户 JavaScript 无服务器云 Deno Deploy 将您的应用部署到边缘。

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

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

接下来是什么?

使用 Deno 和 ImageMagick 构建图片大小调整 API 不仅简单快速,而且可以在 100 行代码内完成。不仅如此,您还可以将此 API 全球部署在边缘,以最大限度地降低 API 消费者的延迟。您可以通过添加文本或将其保存到存储服务等方式来扩展更多功能。

遇到困难或想分享您的工作?来 DiscordTwitter 和我们打个招呼吧!