2022.06.23
Node.jsで画像生成(@napi-rs/canvasを使う場合)【その6: 画像を配置】

この記事でのゴール
Canvasに画像ファイルを読み込んで表示する。
今回はローカルのファイルを利用します。
前提条件
以下をインストール済みであること
- Node.js(16系)
- npm または yarn
使うもの
前回までと同じ
ファイル構成
── output.png
├── package.json
├── src
│ ├── fonts
│ │ └── NotoSansJP-Bold.otf
│ ├── images
│ │ └── img-1.jpg // 読み込み用の画像ファイル
│ ├── index.ts
│ └── libs
│ └── loadImage.ts
読み込みに使用する画像
スクリプト
loadImage.ts
画像ファイルをローカルから読み込むためのスクリプト
import { promises } from 'fs'
export const loadImage = async (url: string) => {
return promises.readFile(url)
}
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
}
const OUTPUT_FILE_NAME: string = 'output' // 出力する画像の名前
const OUTPUT_FILE_EXTENSION: string = 'png' // 出力する画像の拡張子
const OUTPUT_FILE: string = `${OUTPUT_FILE_NAME}.${OUTPUT_FILE_EXTENSION}` // 拡張子を含む画像ファイル名
const CANVAS_WIDTH: number = 800 // 画像のサイズ(幅)
const CANVAS_HEIGHT: number = 800 // 画像のサイズ(高さ)
const CANVAS_BACKGROUND_COLOR: string = 'rgba(20, 30, 200, 0.5)' // 画像の背景色
const IMAGE_URL: string = join(__dirname, 'images', 'img-1.jpg') // 画像ファイルのパス
const canvas = createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT)
const ctx = canvas.getContext('2d')
// 画像を配置
const setImageFile = async () => {
const imageFile = await loadImage(IMAGE_URL)
const canvasImage = new Image()
canvasImage.src = imageFile
canvasImage.width = 800
canvasImage.height = 600
ctx.drawImage(canvasImage, 0, 0)
}
const init = async(): Promise<void> => {
await setImageFile()
const imgData = await canvas.encode('png')
await promises.writeFile(join(__dirname, `../${OUTPUT_FILE}`), imgData)
}
init()
この時点で画像ファイルを生成すると、以下のような画像になります。
このままだと元の画像の一部しか表示されません。
これを、Canvas内にいい感じに収まるようにします。(cssでいうところの background-size: cover; をイメージ )
画像を配置するためにやること
- 元の画像のサイズを取得
- 1から元の画像が縦長であるかを判定
- Canvasに対して元の画像をぴったりに配置するための配置用のサイズを計算する
- 3を利用して、Canvasに対してぴったり配置するための座標を計算する
- 3と4を利用してCanvasにサイズと配置座標を指定した画像を配置
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_WIDTH: number = 800 // 画像のサイズ(幅)
const CANVAS_HEIGHT: number = 800 // 画像のサイズ(高さ)
const CANVAS_BACKGROUND_COLOR: string = 'rgba(20, 30, 200, 0.5)' // 画像の背景色
const IMAGE_URL: string = join(__dirname, 'images', 'img-1.jpg') // 画像ファイルのパス
const canvas = createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT)
const ctx = canvas.getContext('2d')
/**
* 画像が縦長であることを判定する
* @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_WIDTH : imageWidth / imageHeight * CANVAS_WIDTH
const height = isVertical ? imageHeight / imageWidth * width : CANVAS_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_WIDTH / 2 - (imageWidth / 2)
const yPosition = isVertical ? (CANVAS_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 setImageFile = async () => {
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.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()
結果は以下の通りです。
いい感じに配置されるようになりました。