2022.08.06
Node.jsで画像生成(@napi-rs/canvasを使う場合)【その7: 四角形の中に画像を配置】

この記事でのゴール
Canvasに画像ファイルを読み込んで四角形の形で切り取って表示する。
今回はローカルのファイルを利用します。
前提条件
以下をインストール済みであること
- Node.js(16系)
- npm または yarn
使うもの
前回までと同じ
ファイル構成
── output.png
├── package.json
├── src
│ ├── fonts
│ │ └── NotoSansJP-Bold.otf
│ ├── images
│ │ └── img-1.jpg // 読み込み用の画像ファイル
│ ├── index.ts
│ └── libs
│ └── loadImage.ts
読み込みに使用する画像

スクリプト
index.ts
import { promises } from 'fs'
import { join } from 'path'
import { Image, createCanvas, GlobalFonts } from '@napi-rs/canvas'
import { loadImage } from './libs/loadImage'
interface Positions {
x: number
y: number
}
interface Sizes {
width: number,
height: number
}
interface FitSizePositions {
width: number,
height: number,
x: number,
y: number
}
const OUTPUT_FILE_NAME: string = 'output' // 出力する画像の名前
const OUTPUT_FILE_EXTENSION: string = 'png' // 出力する画像の拡張子
const OUTPUT_FILE: string = `${OUTPUT_FILE_NAME}.${OUTPUT_FILE_EXTENSION}` // 拡張子を含む画像ファイル名
const CANVAS_SIZE: Sizes = { width: 800, height: 800 }
const CANVAS_BACKGROUND_COLOR: string = 'rgba(20, 30, 200, 0.5)' // 画像の背景色
const IMAGE_URL: string = join(__dirname, 'images', 'img-1.jpg') // 画像ファイルのパス
// 配置画像のサイズと座標
const CLIP_IMAGE_RECT_SIZE: Sizes = { width: 400, height: 400 }
const CLIP_IMAGE_RECT_POSITION: Positions = { x: 10, y: 10 }
const canvas = createCanvas(CANVAS_SIZE.width, CANVAS_SIZE.height)
const ctx = canvas.getContext('2d')
ctx.fillStyle = CANVAS_BACKGROUND_COLOR
ctx.fillRect(0, 0, CANVAS_SIZE.width, CANVAS_SIZE.height)
/**
* 画像が縦長であることを判定する
* @param imageWidth 元の画像の横の長さ
* @param imageHeight 元の画像の縦の長さ
* @returns boolean
*/
const isVerticalImage = (imageWidth: number, imageHeight: number): boolean => {
return imageWidth < imageHeight
}
/**
* Canvasに対してぴったり収まる画像サイズを取得
* @param imageWidth 元の画像の横の長さ
* @param imageHeight 元の画像の縦の長さ
* @param isVertical 縦長画像であるかどうか
* @returns Sizes
*/
const getFitSizes = (imageWidth: number, imageHeight: number, isVertical: boolean): Sizes => {
const width = isVertical ? CANVAS_SIZE.width : imageWidth / imageHeight * CANVAS_SIZE.width
const height = isVertical ? imageHeight / imageWidth * width : CANVAS_SIZE.height
return { width, height }
}
/**
* Canvasに対してぴったり収まるように配置するための座標を取得
* @param imageWidth getFitSizesで得た配置画像の横の長さ
* @param imageHeight getFitSizesで得た配置画像の縦の長さ
* @param isVertical 縦長の画像であるかどうか
* @returns Positions
*/
const getFitPositions = (imageWidth: number, imageHeight: number, isVertical: boolean): Positions => {
const xPosition = isVertical ? 0 : CANVAS_SIZE.width / 2 - (imageWidth / 2)
const yPosition = isVertical ? (CANVAS_SIZE.height / 2 - imageHeight / 2): 0
return { x: xPosition, y: yPosition }
}
const getFitSizePosition = (imageWidth: number, imageHeight: number): FitSizePositions => {
const isVertical = isVerticalImage(imageWidth, imageHeight)
const sizes = getFitSizes(imageWidth, imageHeight, isVertical)
const positions = getFitPositions(sizes.width, sizes.height, isVertical)
return { width: sizes.width, height: sizes.height, x: positions.x, y: positions.y }
}
const createClipPath = async () => {
ctx.beginPath()
ctx.rect(CLIP_IMAGE_RECT_POSITION.x, CLIP_IMAGE_RECT_POSITION.y, CLIP_IMAGE_RECT_SIZE.width, CLIP_IMAGE_RECT_SIZE.height)
ctx.clip()
}
// 画像を配置
const setImageFile = async () => {
await createClipPath()
const imageFile = await loadImage(IMAGE_URL)
const canvasImage = new Image()
canvasImage.src = imageFile
const imageFitPosition = getFitSizePosition(canvasImage.naturalWidth, canvasImage.naturalHeight)
canvasImage.width = imageFitPosition.width
canvasImage.height = imageFitPosition.height
ctx.fillStyle = 'rgba(0, 0, 0, 1)'
ctx.drawImage(canvasImage, imageFitPosition.x, imageFitPosition.y, imageFitPosition.width, imageFitPosition.height)
}
const init = async(): Promise<void> => {
await setImageFile()
const imgData = await canvas.encode('png')
await promises.writeFile(join(__dirname, `../${OUTPUT_FILE}`), imgData)
}
init()
この時点で画像ファイルを生成すると、以下のような画像になります。

これを、Canvas内に生成した四角形にいい感じに収まるようにします。
画像を配置するためにやること
- 元の画像のサイズを取得
- 1から元の画像が縦長であるかを判定
- Canvasに配置する四角形に対して画像をぴったり配置するためのサイズを計算
- 3を利用して、四角形に対してぴったり配置するための座標を計算する
- 3と4を利用してサイズと配置座標を指定した画像を配置
index.ts
import { promises } from 'fs'
import { join } from 'path'
import { Image, createCanvas, GlobalFonts } from '@napi-rs/canvas'
import { loadImage } from './libs/loadImage'
interface Positions {
x: number
y: number
}
interface Sizes {
width: number,
height: number
}
interface FitSizePositions {
width: number,
height: number,
x: number,
y: number
}
const OUTPUT_FILE_NAME: string = 'output' // 出力する画像の名前
const OUTPUT_FILE_EXTENSION: string = 'png' // 出力する画像の拡張子
const OUTPUT_FILE: string = `${OUTPUT_FILE_NAME}.${OUTPUT_FILE_EXTENSION}` // 拡張子を含む画像ファイル名
const CANVAS_SIZE: Sizes = { width: 800, height: 800 }
const CANVAS_BACKGROUND_COLOR: string = 'rgba(20, 30, 200, 0.5)' // 画像の背景色
const IMAGE_URL: string = join(__dirname, 'images', 'img-1.jpg') // 画像ファイルのパス
// 配置画像のサイズと座標
const CLIP_IMAGE_RECT_SIZE: Sizes = { width: 400, height: 400 }
const CLIP_IMAGE_RECT_POSITION: Positions = { x: 10, y: 10 }
const canvas = createCanvas(CANVAS_SIZE.width, CANVAS_SIZE.height)
const ctx = canvas.getContext('2d')
ctx.fillStyle = CANVAS_BACKGROUND_COLOR
ctx.fillRect(0, 0, CANVAS_SIZE.width, CANVAS_SIZE.height)
/**
* 画像が縦長であることを判定する
* @param imageWidth 元の画像の横の長さ
* @param imageHeight 元の画像の縦の長さ
* @returns boolean
*/
const isVerticalImage = (imageWidth: number, imageHeight: number): boolean => {
return imageWidth < imageHeight
}
/**
* 指定したサイズに対してぴったり収まる画像サイズを取得
* @param imageWidth 元の画像の横の長さ
* @param imageHeight 元の画像の縦の長さ
* @param isVertical 縦長画像であるかどうか
* @param baseWidth 基点とする横の長さ
* @param baseHeight 基点とする縦の長さ
* @returns Sizes
*/
const getFitSizes = (imageWidth: number, imageHeight: number, isVertical: boolean, baseWidth: number, baseHeight: number): Sizes => {
const width = isVertical ? baseWidth : imageWidth / imageHeight * baseWidth
const height = isVertical ? imageHeight / imageWidth * width : baseHeight
return { width, height }
}
/**
* 指定したサイズに対してぴったり収まるように配置するための座標を取得
* @param imageWidth getFitSizesで得た配置画像の横の長さ
* @param imageHeight getFitSizesで得た配置画像の縦の長さ
* @param baseWidth 基点とする横の長さ
* @param baseHeight 基点とする縦の長さ
* @param isVertical 縦長の画像であるかどうか
* @returns Positions
*/
const getFitPositions = (imageWidth: number, imageHeight: number, isVertical: boolean, baseWidth: number, baseHeight: number): Positions => {
const xPosition = isVertical ? 0 : baseWidth / 2 - (imageWidth / 2)
const yPosition = isVertical ? (baseHeight / 2 - imageHeight / 2): 0
return { x: xPosition, y: yPosition }
}
const getFitSizePosition = (imageWidth: number, imageHeight: number, baseWidth: number, baseHeight: number): FitSizePositions => {
const isVertical = isVerticalImage(imageWidth, imageHeight)
const sizes = getFitSizes(imageWidth, imageHeight, isVertical, baseWidth, baseWidth)
const positions = getFitPositions(sizes.width, sizes.height, isVertical, baseWidth, baseHeight)
return { width: sizes.width, height: sizes.height, x: positions.x, y: positions.y }
}
const createClipPath = async () => {
ctx.beginPath()
ctx.rect(CLIP_IMAGE_RECT_POSITION.x, CLIP_IMAGE_RECT_POSITION.y, CLIP_IMAGE_RECT_SIZE.width, CLIP_IMAGE_RECT_SIZE.height)
ctx.clip()
}
// 画像を配置
const setImageFile = async () => {
await createClipPath()
const imageFile = await loadImage(IMAGE_URL)
const canvasImage = new Image()
canvasImage.src = imageFile
const imageFitPosition = getFitSizePosition(canvasImage.naturalWidth, canvasImage.naturalHeight, CLIP_IMAGE_RECT_SIZE.width, CLIP_IMAGE_RECT_SIZE.height)
canvasImage.width = imageFitPosition.width
canvasImage.height = imageFitPosition.height
ctx.fillStyle = 'rgba(0, 0, 0, 1)'
ctx.drawImage(canvasImage, imageFitPosition.x, imageFitPosition.y, imageFitPosition.width, imageFitPosition.height)
}
const init = async(): Promise<void> => {
await setImageFile()
const imgData = await canvas.encode('png')
await promises.writeFile(join(__dirname, `../${OUTPUT_FILE}`), imgData)
}
init()
結果は以下の通りです。

いい感じに配置されるようになりました。