👾

Figma プラグインで画像を crop する(あるいは Canvas で UintArray な画像を crop する方法)

2022/02/22に公開

Figma プラグインの Image では getBytesAsync() という関数を使えば画像のデータを取得することができるのですが、実は crop をした場合でも crop される前のオリジナルのデータが取得されます。
https://www.figma.com/plugin-docs/api/Image/

crop 後のデータが欲しい場合があったのでその時に書いたコードを紹介していきます。

const image = figma.getImageByHash(imagePaint.imageHash || '');
const bytes = await image?.getBytesAsync();

// まず crop されているかどうかを imageTransform に値が入っているかどうかで判定します。
if (imagePaint.imageTransform && imagePaint.imageTransform.length > 0) {
	// Crop する領域を求める & getBytesAsync() で返される画像データは元のサイズとして書き出される(例えば 3000px の画像を import してから 200px に縮小した時、getBytesAsync では 3000px として書き出されるが、node の width や height などは 200px)ので crop 前の画像サイズを求めます
	// imageTransform は affine transform という形で表現されています
	// [0][0] の部分が x の crop 前の画像からの比率 [1][1] の部分が y の crop 前の画像からの比率
	// [0][2] の部分が tx [1][2] の部分が ty を表します。
	// See: https://www.figma.com/plugin-docs/api/Transform/

	const originalWidth = this.node.width / transform[0][0];
            const originalHeight = this.node.height / transform[1][1];	
	const xOffset = originalWidth * imagePaint.imageTransform[0][2];
	const yOffset = originalHeight * imagePaint.imageTransform[1][2];

	// Crop 処理は Canvas 要素を使う必要があるので UI スレッドに message を送ります
	figma.ui.postMessage({
		type: "cropImage",
		imgByteArray: bytes,
		xOffset,
		yOffset,
		width: this.node.width,
		height: this.node.height,
		imageWidth: originalWidth,
		imageHeight: originalHeight,
	});

	// Crop 処理した結果が渡ってくるまで待ちます
	const waitUntilImageCropped: Promise<string> = new Promise(
		(resolve, reject) => {
		  figma.ui.onmessage = (msg: {
		    type: MessageType;
		    base64Str: string;
		  }) => {
		    if (msg.type === 'notify-cropped-image') {
		      resolve(msg.base64Str);
		    }
		  };
		  // 無限に待つとアレなので5秒でタイムアウトするようにしています
		  setTimeout(() => reject(), 5000);
		}
	);
	return await waitUntilImageCropped;
}

次に UI スレッド側で Canvas を用いて Crop するところを見ていきます!

こちらのように drawImage で表示したい領域を指定することによって実現できます。

https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage

if (event.data.pluginMessage.type === 'cropImage') {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  
  // 渡ってきた bytes を元に Image オブジェクトを作ります。
  const url = URL.createObjectURL(
    new Blob([event.data.pluginMessage.imgByteArray])
  );
  const imagePromise = new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = () => reject();
    img.src = url;
  });

  imagePromise.then((image) => {
    const { xOffset, yOffset, width, height, imageWidth, imageHeight } =
      event.data.pluginMessage;

    // 先ほど書いたように元の画像サイズ(bytes で表現されているもの)と現在の画像サイズが違うことがあるので、 Canvas に正しい比率で書き込めるよう比率を求めます
    const xScale = image.width / imageWidth;
    const yScale = image.height / imageHeight;

    canvas.width = width;
    canvas.height = height;
    // Canvas に Crop 領域を定めつつ画像を書き込みます。
    ctx.drawImage(
      image,
      xOffset * xScale,
      yOffset * yScale,
      width * xScale,
      height * yScale,
      0,
      0,
      width,
      height
    );

        // あとは書き込んだものを base64 の文字列として返します!
    const msg = {
      type: 'notify-cropped-image',
      base64Str: canvas.toDataURL('image/png'),
    };
    parent.postMessage({ pluginMessage: msg }, '*');
  });

Discussion