👾
Figma プラグインで画像を crop する(あるいは Canvas で UintArray な画像を crop する方法)
Figma プラグインの Image では getBytesAsync()
という関数を使えば画像のデータを取得することができるのですが、実は crop をした場合でも crop される前のオリジナルのデータが取得されます。
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
で表示したい領域を指定することによって実現できます。
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