ゲーム開発
Unity
UnrealEngine
C++
ゲーム数学
ゲームAI
サウンド
アニメーション
GBDK
制作日記
3DCG
Houdini
Blender
USD
グラフィックス
テクノロジ
ツール開発
フロントエンド関連
サーバサイド関連
ソフトウェア設計
ハードウェア関連
おすすめ技術書
音楽
DTM
楽器・機材
ピアノ
その他
都会のエレキベア
ラーメン日記
四コマ漫画
おすすめアイテム
おもしろコラム
  • ゲーム開発
    • Unity
    • UnrealEngine
    • C++
    • ゲーム数学
    • ゲームAI
    • サウンド
    • アニメーション
    • GBDK
    • 制作日記
  • 3DCG
    • Houdini
    • Blender
    • USD
    • グラフィックス
  • テクノロジ
    • ツール開発
    • フロントエンド関連
    • サーバサイド関連
    • ソフトウェア設計
    • ハードウェア関連
    • おすすめ技術書
  • 音楽
    • DTM
    • 楽器・機材
    • ピアノ
  • その他
    • 都会のエレキベア
    • ラーメン日記
    • 四コマ漫画
    • おすすめアイテム
    • おもしろコラム
  1. ホーム
  2. 20251122_01_vscode_image_resizer

【VSCode】ドラッグ&ドロップで画像ファイルをリサイズ・保存する拡張機能を作る

ツール開発JavaScriptVSCodeVSCode拡張機能TypeScript
2025-11-22

開発の経緯

マイケル
マイケル
みなさんこんにちは! まいけるです!
エレキベア
エレキベア
こんにちクマ~~~
マイケル
マイケル
今回は ブログ執筆用ツール に関しての話です。 このブログはMarkdownを使用して執筆していて、これまで画像ファイルを扱うのに以下のようなツールを開発して使用していました。
【Next.js】第三回 WordPressブログをNext.jsに移行する 〜Markdown執筆環境構築編〜
2023-12-31
【Electron × Vue3】画像をリサイズして任意の場所に保存するツールを作る
2023-12-31
20231231_image_resize_01
▲以前作成した画像リサイズツール

エレキベア
エレキベア
なつかしいクマね~~~ Electronを使って開発したクマね
マイケル
マイケル
しばらくこのツールを使用していて以前より快適になっていたのですが、 以下のような問題がありました・・・。
  • 既存ツールの問題
    • ツール、Explorer(Finder)など複数ウィンドウ立ち上げることになり、画面がごちゃつく
    • 画像リサイズ後、Markdownに文字のコピーペーストを手動で行う必要があり面倒
マイケル
マイケル
ちょっとした問題に思えますが、画像ファイルは大量に扱うのでトータルで考えると中々に効率が悪い です。
エレキベア
エレキベア
塵も積もれば・・・のやつクマね
マイケル
マイケル
そんなことから「VSCodeに直接D&Dして貼り付け出来るのが一番よさそう」というのを薄々感づいていたのですが、VSCodeの拡張機能を自作すれば実現できそう というのが分かったので挑戦してみることにしました。
マイケル
マイケル
そんなこんなで今回作ってみたのがこちらです!
20251122_01_01
▲今回作成した拡張機能

マイケル
マイケル
Markdown上に画像ファイルを直接D&Dしてサイズを入力 すると、リサイズした画像の保存と文字のペーストまで一括で行う機能 になっています。
エレキベア
エレキベア
だいぶシンプルで使いやすくなったクマね
マイケル
マイケル
今回はVSCodeの拡張機能を作るのも初めてだったのでそもそもの開発方法も含めて、どのように実装して実現したか を解説していこうと思います! GitHubリポジトリにも上げているので、こちらもあわせてご参照ください。

GitHub - vscode-image-resizer / plasmo310
▲作成したVSCode拡張機能

エレキベア
エレキベア
楽しみクマ~~~

VSCode拡張機能の開発方法

マイケル
マイケル
まずそもそものVSCode拡張機能の作り方についてですが、 以下の公式ドキュメントに手順がまとまっています。

参考:
Your First Extension | Visual Studio Code Extension API

エレキベア
エレキベア
手順もだいぶシンプルクマね
マイケル
マイケル
基本的には公式ドキュメントを見るのがいいと思いますが、簡単な手順だけ記載しておきます。

拡張機能テンプレートの作成

マイケル
マイケル
拡張機能のテンプレートは、yo、generator-codeパッケージを入れてコマンド実行することで作ることが出来ます。 以下のいずれかの方法でテンプレート作成コマンドを実行します。
# yo、generator-codeのインストール
npm install -g yo generator-code

# テンプレート作成
yo code
▲グローバルインストールして実行する場合
# npxコマンドで実行する場合
npx --package yo --package generator-code -- yo code
▲ローカル環境を汚したくない場合
マイケル
マイケル
実行すると諸々聞かれますが、今回は以下のようにTypeScriptプロジェクトとして作成しました。
# 今回は以下のような形で作成
# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? image-resizer
### Press <Enter> to choose default for all options below ###

# ? What's the identifier of your extension? image-resizer
# ? What's the description of your extension? Image Resize Tool.
# ? Initialize a git repository? Y
# ? Which bundler to use? unbundled
# ? Which package manager to use? npm

# ? Do you want to open the new folder with Visual Studio Code? Open with `code`
▲テンプレート作成の設定
エレキベア
エレキベア
テンプレート作成してくれるのはありがたいクマ

デフォルト実装の確認

マイケル
マイケル
最初に作成されるテンプレートについては、HelloWorldと出力するだけのコマンドを登録するだけのもの になっています。 メイン処理は src/extension.ts で、実行するコマンドとの紐づけは package.json の中に記載されています。
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {

	console.log('Congratulations, your extension "image-resizer" is now active!');

	// Register Command.
	const disposable = vscode.commands.registerCommand('image-resizer.helloWorld', () => {
		vscode.window.showInformationMessage('Hello World from image-resizer!');
	});

	context.subscriptions.push(disposable);
}

export function deactivate() {}
▲HelloWorldコマンドの登録処理
・・・略・・・
  "contributes": {
    "commands": [
      {
        "command": "image-resizer.helloWorld",
        "title": "Hello World"
      }
    ]
  },
  ・・・略・・・
▲package.jsonにコマンドの紐づけが記載されている
エレキベア
エレキベア
activateイベントでコマンド登録するだけの、非常にシンプルな実装クマね

デバッグ方法

マイケル
マイケル
開発中の拡張機能をどのように確認するかですが、F5キーでデバッグ実行 することで別のVSCodeが開き、挙動を確認することができます。
  1. src/extension.tsを開いた状態でF5キーを押下する
  2. デバッグ用のVSCodeが別で開き、処理の確認が行える
20251122_01_02
▲F5キー押下でデバッグ実行できる

エレキベア
エレキベア
いちいちパッケージングしなくても確認できるのクマね

パッケージング

マイケル
マイケル
最後に開発が完了した拡張機能を配布して使用出来るようにするには、VSIX形式のファイルとしてパッケージング する必要があります。 こちらはpackage.jsonに必要な情報を記載した後、vsceパッケージを使用してコマンド実行することで実行出来ます。
  1. package.jsonの記入

    {
    "name": "image-resizer",
    "displayName": "Image Resizer",
    "description": "Image Resize Tool.",
    "version": "0.0.1",
    "repository": {
        "type": "git",
        "url": "https://github.com/plasmo310/vscode-image-resizer"
    },
    "engines": {
        "vscode": "^1.106.1"
    },
    ...
    }
    
    ▲publisher、repository等の情報を入力する
  2. vsceを使用してパッケージコマンドを実行する

    # (未インストールの場合) vsceをインストールする
    npm install -g @vscode/vsce
    
    # パッケージコマンドの実行
    vsce package
    
  3. 作成されたVSIXファイルを拡張機能としてインストールする

マイケル
マイケル
作成したVSIXファイルは、VSCode上から拡張機能としてインストールすることで使用できるようになります。
20251122_01_03
▲パッケージコマンドを実行するとVSIXファイルが作成される

20251122_01_04
▲拡張機能としてVSIXファイルをインストールして使用する

マイケル
マイケル
ローカルで開発・使用するにあたる、基本的な開発方法としては以上になります。
エレキベア
エレキベア
フロントエンド開発している人にはそこまで敷居が高くないクマね

画像リサイズ・保存機能の実装

マイケル
マイケル
それでは実際に今回の画像リサイズ・保存機能についての実装内容について解説します。

実装する機能の概要

マイケル
マイケル
まず実装する機能については、冒頭で触れた通り以下のようになります。
  • 実装する機能
    • VSCodeで開いたMarkdown上に画像ファイルを直接ドラッグ&ドロップして使用できる
    • 入力した横幅を元にリサイズを行い、指定フォルダ配下に保存する
    • リサイズ情報から整形したテキストをMarkdown上にペーストする
エレキベア
エレキベア
画像ファイルをVSCodeにD&Dしてリサイズ・保存する機能クマね

画像操作ヘルパークラスの用意

マイケル
マイケル
まずは拡張機能に関係ない部分で、画像ファイルの操作を行う処理を用意しました。 sharpライブラリを使用して、 ・画像情報の取得 ・比率固定でサイズ計算 ・リサイズ・保存処理 をそれぞれヘルパー関数として用意しています。
import sharp from "sharp";
import * as path from "path";

/**
 * 画像情報
 */
export interface ImageInfo {
    path: string;
    name: string;
    extension: string;
    width: number;
    height: number;
    ratio: number;
}

/**
 * 画像関連ヘルパークラス
 */
export class ImageHelper {
    /**
     * バッファから画像情報取得
     */
    static async loadImageInfo(
        imageBuffer: Buffer,
        filePath: string
    ): Promise<ImageInfo> {
        const fullName = path.basename(filePath);
        const ext = path.extname(fullName).replace(".", "").toLowerCase();
        const name = path.basename(fullName, path.extname(fullName));

        const metadata = await sharp(imageBuffer).metadata();
        if (!metadata.width || !metadata.height) {
            throw new Error("画像サイズ取得に失敗しました");
        }

        return {
            path: filePath,
            name,
            extension: ext,
            width: metadata.width,
            height: metadata.height,
            ratio: metadata.width / metadata.height,
        };
    }

    /**
     * 横幅から比率固定で高さを計算
     */
    static calcHeightByWidth(
        targetWidth: number,
        ratio: number
    ): number {
        return Math.round(targetWidth / ratio)
    }

    /**
     * リサイズしてファイルとして保存
     */
    static async resizeAndSave(options: {
        imageBuffer: Buffer,
        outputPath: string,
        width: number,
        height: number
    }): Promise<void> {
        const { imageBuffer: buffer, outputPath, width, height } = options;

        await sharp(buffer)
            .resize({
                width,
                height: height ?? undefined,
            })
            .toFile(outputPath);
    }
}

▲sharpライブラリを使用した画像処理
エレキベア
エレキベア
これらの処理を各イベントで呼び出すようにするクマね

VSCode内イベントとの紐づけ

マイケル
マイケル
VSCode内でドラッグ&ドロップした際のイベントとの紐づけ処理 は以下のようになります。
import * as vscode from 'vscode';
import * as path from "path";
import { ImageHelper } from './helper/image-helper';


/**
 * フォーマット文字列に対して変数を展開する
 * @param format フォーマット文字列
 * @param vars 変数マップ
 * @returns 展開後の文字列
 */
function expandTemplate(format: string, vars: Record<string, string>): string {
	return format.replace(/\$\{(\w+)\}/g, (_, key) => vars[key] ?? "");
}

/**
 * 挿入テキスト用の幅を取得する
 * 実際の画像サイズより少し小さめの値に調整する
 * @param width 元の幅
 * @returns 挿入テキスト用の幅
 */
function getInsertTextWidth(width: number): number {
	// 入力以下の最大値を返却する
	const WIDTH_SETTINGS = [
		360,
		480,
		600,
		800,
		920
	]
	const candidates = WIDTH_SETTINGS.filter(w => w <= width);
	if (candidates.length === 0) {
		// 該当なし → 入力値そのまま
		return width;
	}
	return Math.max(...candidates);
}

/**
 * アクティベート時処理
 * @param context 
 */
export function activate(context: vscode.ExtensionContext) {

	const provider: vscode.DocumentDropEditProvider = {
		async provideDocumentDropEdits(document, _position, dataTransfer, _token) {

			// 設定の取得
			const config = vscode.workspace.getConfiguration("imageResizer");
			const outputImageDir = config.get<string>("outputImageDir");
			const insertTextFormat = config.get<string>("insertTextFormat");
			if (!outputImageDir) {
				vscode.window.showErrorMessage("出力先フォルダ 'imageResizer.outputImageDir' を設定してください。");
				return;
			}
			if (!insertTextFormat) {
				vscode.window.showErrorMessage("挿入テキスト形式 'imageResizer.insertTextFormat' を設定してください。");
				return;
			}

			// D&Dされたファイルを抽出
			const files: vscode.DataTransferFile[] = [];
			dataTransfer.forEach((item) => {
				const f = item.asFile?.();
				if (f) files.push(f);
			});
			if (files.length === 0) {
				return;
			}

			// 1つ目の画像ファイルを処理対象とする
			const file = files[0];
			if (!file || !file.uri) {
				return;
			}
			const extname = path.extname(file.uri.fsPath || "").toLowerCase();
			if (![".png", ".jpg", ".jpeg", ".gif", ".webp"].includes(extname)) {
				return;
			}

			// 画像データの読み込み
			const imageData = await file.data();
			const imageBuffer = Buffer.from(imageData);

			// 画像情報の取得
			let imageInfo;
			try {
				imageInfo = await ImageHelper.loadImageInfo(
					imageBuffer,
					file.uri.fsPath || file.uri.path
				);
				console.log("[Size] width: ", imageInfo.width, " height: ", imageInfo.height);

			} catch (e) {
				vscode.window.showErrorMessage("画像サイズの取得に失敗しました。");
				console.error(e);
				return;
			}

			// 幅入力メッセージの表示
			const inputWidthString: string | undefined = await vscode.window.showInputBox({
				prompt: "リサイズ後の幅 (px) を入力",
				placeHolder: "例: 600",
				value: imageInfo.width.toString(),
				validateInput: (value: string) => {
					const n = Number(value);
					if (value.trim() === "" || !Number.isInteger(n) || n <= 0) {
						return "正の整数を入力してください";
					}
					return null;
				},
			});
			if (inputWidthString === undefined) {
				return;
			}

			// 画像リサイズ・保存
			const newWidth: number = Number(inputWidthString);
			const newHeight: number = ImageHelper.calcHeightByWidth(
				newWidth,
				imageInfo.ratio
			);
			try {
				const outputPath = path.join(outputImageDir, imageInfo.name + extname);
				ImageHelper.resizeAndSave({
					imageBuffer: imageBuffer,
					outputPath,
					width: newWidth,
					height: newHeight,
					extension: imageInfo.extension
				});
				console.log("[ReSize] width: ", newWidth, " height: ", newHeight);
				console.log(`[Output] ${outputPath}`);

			} catch (e) {
				vscode.window.showErrorMessage("画像ファイルの保存に失敗しました。");
				console.error(e);
				return;
			}

			// テキスト挿入
			const insertText = expandTemplate(insertTextFormat, {
				filename: imageInfo.name,
				extension: imageInfo.extension,
				width: getInsertTextWidth(newWidth).toString(),
			});
			return new vscode.DocumentDropEdit(insertText);
		}
	};

	// Providerを登録
	const selector: vscode.DocumentSelector = {
		scheme: "file",
		language: "markdown",
	};
	const disposable = vscode.languages.registerDocumentDropEditProvider(
		selector,
		provider
	);
	context.subscriptions.push(disposable);
}

export function deactivate() { }

エレキベア
エレキベア
ここがメイン処理にあたるのクマね
マイケル
マイケル
コードは少し長いので折りたたみにしてあります。 この後それぞれの処理について軽く記載します。
ドラッグ&ドロップの検知
マイケル
マイケル
ドラッグ&ドロップ時の処理はDocumentDropEditProviderをカスタムして設定することで制御できます。 Markdownファイルのみを対象に、単一ファイルを対象に処理を行 うようにしています。
import * as vscode from 'vscode';
import * as path from "path";
import { ImageHelper } from './helper/image-helper';

・・・略・・・

/**
 * アクティベート時処理
 * @param context 
 */
export function activate(context: vscode.ExtensionContext) {

	const provider: vscode.DocumentDropEditProvider = {
		async provideDocumentDropEdits(document, _position, dataTransfer, _token) {

・・・略・・・

			// D&Dされたファイルを抽出
			const files: vscode.DataTransferFile[] = [];
			dataTransfer.forEach((item) => {
				const f = item.asFile?.();
				if (f) files.push(f);
			});
			if (files.length === 0) {
				return;
			}

			// 1つ目の画像ファイルを処理対象とする
			const file = files[0];
			if (!file || !file.uri) {
				return;
			}
			const extname = path.extname(file.uri.fsPath || "").toLowerCase();
			if (![".png", ".jpg", ".jpeg", ".gif", ".webp"].includes(extname)) {
				return;
			}

・・・略・・・

			// テキスト挿入
			const insertText = expandTemplate(insertTextFormat, {
				filename: imageInfo.name,
				extension: imageInfo.extension,
				width: getInsertTextWidth(newWidth).toString(),
			});
			return new vscode.DocumentDropEdit(insertText);
		}
	};

	// Providerを登録
	const selector: vscode.DocumentSelector = {
		scheme: "file",
		language: "markdown",
	};
	const disposable = vscode.languages.registerDocumentDropEditProvider(
		selector,
		provider
	);
	context.subscriptions.push(disposable);
}

export function deactivate() { }

▲ドラッグ&ドロップ検知
マイケル
マイケル
最後に作成しているinsertTextは、Markdownに挿入されるテキストになります。 こちらを成形することで好きな文言でペーストすることができます。
エレキベア
エレキベア
まさしく求めていた機能クマ
ツール設定の取得
マイケル
マイケル
保存先フォルダと挿入テキストについては、VSCode設定で指定出来るようにしています。
{

・・・略・・・

          // VSCode Image Resizer Settings
          "imageResizer.outputImageDir": "c:\\blog\\public\\content",
          "imageResizer.insertTextFormat": "![${filename}{width:${width}px}](/content/${filename}.${extension} \"${filename}\")"
  }
  
▲settings.json内で指定できるようにしている
マイケル
マイケル
設定項目は package.json 内で定義できるのと、取得はvscode.workspace.getConfigurationで取得したconfig経由 で行います。

・・・略・・・

  "contributes": {
    "title": "Image Resizer",
    "configuration": {
      "properties": {
        "imageResizer.outputImageDir": {
          "type": "string",
          "default": "",
          "description": "Output directory for resized images."
        },
        "imageResizer.insertTextFormat": {
          "type": "string",
          "default": "![${filename}](${filepath})",
          "description": "Format for inserting resized image markdown."
        }
      }
    }
  },

・・・略・・・


・・・略・・・

			// 設定の取得
			const config = vscode.workspace.getConfiguration("imageResizer");
			const outputImageDir = config.get<string>("outputImageDir");
			const insertTextFormat = config.get<string>("insertTextFormat");
			if (!outputImageDir) {
				vscode.window.showErrorMessage("出力先フォルダ 'imageResizer.outputImageDir' を設定してください。");
				return;
			}
			if (!insertTextFormat) {
				vscode.window.showErrorMessage("挿入テキスト形式 'imageResizer.insertTextFormat' を設定してください。");
				return;
			}

・・・略・・・

エレキベア
エレキベア
settings.jsonで指定できると環境ごとに設定を変えれて便利クマね
画像情報取得・幅の入力メッセージ表示
マイケル
マイケル
用意したヘルパー関数で画像情報を取得して、その情報を元にリサイズ後の幅をユーザに入力させます。 こちらは vscode.window.showInputBox を呼び出すことで入力欄を表示させることができます。

・・・略・・・

			// 画像情報の取得
			let imageInfo;
			try {
				imageInfo = await ImageHelper.loadImageInfo(
					imageBuffer,
					file.uri.fsPath || file.uri.path
				);
				console.log("[Size] width: ", imageInfo.width, " height: ", imageInfo.height);

			} catch (e) {
				vscode.window.showErrorMessage("画像サイズの取得に失敗しました。");
				console.error(e);
				return;
			}

			// 幅入力メッセージの表示
			const inputWidthString: string | undefined = await vscode.window.showInputBox({
				prompt: "リサイズ後の幅 (px) を入力",
				placeHolder: "例: 600",
				value: imageInfo.width.toString(),
				validateInput: (value: string) => {
					const n = Number(value);
					if (value.trim() === "" || !Number.isInteger(n) || n <= 0) {
						return "正の整数を入力してください";
					}
					return null;
				},
			});
			if (inputWidthString === undefined) {
				return;
			}

・・・略・・・

20251122_01_05
▲VSCode上で入力欄が表示される

エレキベア
エレキベア
ここもVSCodeの機能を使って入力できるクマね
画像リサイズ・保存処理
マイケル
マイケル
最後に画像リサイズ・保存処理ですが、こちらもヘルパー関数を呼び出しているだけですね。

・・・略・・・

			// 画像リサイズ・保存
			const newWidth: number = Number(inputWidthString);
			const newHeight: number = ImageHelper.calcHeightByWidth(
				newWidth,
				imageInfo.ratio
			);
			try {
				const outputPath = path.join(outputImageDir, imageInfo.name + extname);
				ImageHelper.resizeAndSave({
					imageBuffer: imageBuffer,
					outputPath,
					width: newWidth,
					height: newHeight,
					extension: imageInfo.extension
				});
				console.log("[ReSize] width: ", newWidth, " height: ", newHeight);
				console.log(`[Output] ${outputPath}`);

			} catch (e) {
				vscode.window.showErrorMessage("画像ファイルの保存に失敗しました。");
				console.error(e);
				return;
			}

・・・略・・・

エレキベア
エレキベア
ロジックが分離されていると分かりやすいクマね
マイケル
マイケル
主要となる実装の解説は以上になります!

おわりに

マイケル
マイケル
というわけで今回はVSCode拡張機能の開発についてでした! どうだったかな?
エレキベア
エレキベア
なんとなく難しいイメージがあったクマが、 開発環境は割と整っていて手軽に開発できたクマね
マイケル
マイケル
こういった拡張が手軽に行えるのもVSCodeの強みだね! みなさんもぜひ開発効率を上げるための機能を実装してみましょう!
マイケル
マイケル
それでは今日はこの辺で! アデューー!!
エレキベア
エレキベア
クマ~~~~~

【VSCode】ドラッグ&ドロップで画像ファイルをリサイズ・保存する拡張機能を作る ~完~


ツール開発JavaScriptVSCodeVSCode拡張機能TypeScript
2025-11-22

関連記事
【Unity】Timeline × Excelでスライドショーを効率よく制作する
2024-10-31
【ゲーム数学】第九回 p5.jsで学ぶゲーム数学「フーリエ解析」
2024-05-12
【Node.js】廃止されたAmazonアソシエイト画像リンクをAmazon Product Advertising API経由で復活させる
2024-01-08
【都会のエレキベア】ブログを大幅リニューアル!WordPressからNext.jsに移行するまでの流れをまとめる
2024-01-01
【Next.js】第四回 WordPressブログをNext.jsに移行する 〜サーバ移行・SEO・広告設定編〜
2023-12-31
【Next.js】第三回 WordPressブログをNext.jsに移行する 〜Markdown執筆環境構築編〜
2023-12-31
【Next.js】第二回 WordPressブログをNext.jsに移行する 〜WordPressデータの移行・表示編〜
2023-12-31
【Next.js】第一回 WordPressブログをNext.jsに移行する 〜全体設計、環境構築編〜
2023-12-31