👏

Obsidianで画像のコピペを自動でWebPに変換するプラグインを作った

に公開

はじめに

Obsidianで画像を多用していると、Vaultのサイズがどんどん膨れ上がっていく経験はないでしょうか。特にスクリーンショットを頻繁に貼り付ける場合、PNGやJPEG形式の画像ファイルがストレージを圧迫します。

この問題を解決するため、クリップボードから画像をペーストする際に自動的にWebP形式に変換するObsidianプラグイン「Paste Image as WebP」を開発しました。本記事では、WebP形式の利点とプラグインの機能、そして開発の過程について解説します。

WebP形式の利点

WebPは、Googleが開発した画像フォーマットで、従来のPNGやJPEGと比較して以下のような優位性があります。

ファイルサイズの削減

WebPの最大の特徴は、高い圧縮率です。同程度の画質を保ちながら、PNGやJPEGと比較して20〜50%程度ファイルサイズを削減できます。

具体的な例を挙げると、4096×4096ピクセルのスクリーンショットの場合:

  • PNG形式:約8〜12MB
  • JPEG形式(品質85%):約2〜4MB
  • WebP形式(品質85%):約1〜2MB

この差は、画像を多用するVaultでは大きな影響を与えます。

透過対応

WebPは、PNG同様にアルファチャンネル(透過)をサポートしています。そのため、背景が透明な画像もそのまま扱うことができます。

ブラウザ対応

現在、主要なブラウザはすべてWebPをサポートしており、Obsidianでも問題なく表示できます。

プラグインの機能

基本機能

「Paste Image as WebP」プラグインは、以下の基本機能を提供します。

  1. 自動変換:クリップボードから画像をペースト(Ctrl+V / Cmd+V)すると、自動的にWebP形式に変換して保存
  2. エディタへの自動挿入:変換後の画像を![[ファイル名.webp]]形式で自動的にエディタに挿入
  3. 重複ファイルの処理:同名ファイルが存在する場合、自動的に連番を追加(例:image-1.webp, image-2.webp

ファイル名のカスタマイズ

ファイル名は2つの形式から選択できます。

固定名形式

同じファイル名で保存します。例えば、image.webpという固定名を設定すると、すべての画像がこの名前で保存されます(重複時は自動的に連番が追加されます)。

タイムスタンプ形式

日時をベースにしたファイル名を生成します。フォーマットは自由にカスタマイズ可能で、以下のプレースホルダーを使用できます。

  • YYYY:年(4桁)
  • MM:月(2桁)
  • DD:日(2桁)
  • HH:時(2桁、24時間形式)
  • mm:分(2桁)
  • ss:秒(2桁)

デフォルトのYYYYMMDDHHmmss形式では、20231116143025.webpのようなファイル名が生成されます。カスタム例として、YYYY-MM-DD_HHmmssとすると2023-11-16_143025.webpという形式になります。

保存先の設定

画像の保存先は3つのオプションから選択できます。

  1. 現在のノートと同じフォルダ:ノートと同じディレクトリに指定したフォルダ名(デフォルト:attachments)を作成して保存
  2. Vaultルート:Vaultのルートに指定したフォルダを作成して保存
  3. カスタムパス:任意のパスを指定(例:project/images

WebP品質の調整

WebPの品質は0.1〜1.0の範囲で調整できます。デフォルトは0.85で、ファイルサイズと画質のバランスが取れた値です。

  • 1.0:最高品質(ファイルサイズ大)
  • 0.85:推奨値(バランス型)
  • 0.5〜0.7:低品質(ファイルサイズ重視)

セキュリティ機能

プラグインには、以下のセキュリティ対策が実装されています。

  1. パストラバーサル攻撃の防止:ファイル名とフォルダパスをサニタイズし、Vault外部への書き込みを防止
  2. DoS攻撃の防止:画像の最大ピクセル数(デフォルト:16,777,216 = 4096×4096)と最大ファイルサイズ(デフォルト:10MB)を制限
  3. ファイル名インジェクションの防止:特殊文字を適切に処理
  4. 無限ループの防止:重複ファイル名チェックの試行回数を制限

これらの設定は、プラグインの設定画面から変更できます。

技術的な実装

開発環境

プラグインは以下の技術スタックで開発しました。

  • 言語:TypeScript
  • ビルドツール:esbuild
  • ターゲット環境:Obsidian Plugin API

プロジェクト構成

obsidian_extension/
├── main.ts              # メインのプラグインロジック
├── manifest.json        # プラグインのメタデータ
├── package.json         # npm設定
├── tsconfig.json        # TypeScript設定
├── esbuild.config.mjs   # esbuild設定
└── styles.css           # スタイルシート

TypeScriptからJavaScriptへのビルド

Obsidianプラグインは、最終的にJavaScriptファイル(main.js)として配布する必要があります。開発ではTypeScriptを使用し、esbuildでバンドル・トランスパイルしています。

package.jsonのスクリプト設定:

{
  "scripts": {
    "dev": "node esbuild.config.mjs",
    "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
  }
}

開発時はnpm run devでファイル変更を監視し、自動的にリビルドされます。本番ビルドはnpm run buildで実行します。

esbuildの設定

esbuildは高速なJavaScriptバンドラーで、TypeScriptのトランスパイルも行えます。設定ファイル(esbuild.config.mjs)では、以下を指定しています。

{
  entryPoints: ["main.ts"],
  bundle: true,
  external: ["obsidian", "electron", ...],
  format: "cjs",
  target: "es2018",
  outfile: "main.js"
}

重要なポイント:

  • external:Obsidianが提供するモジュールは外部依存として扱う
  • format: "cjs":CommonJS形式で出力
  • target: "es2018":ES2018をターゲットに設定

画像変換のコアロジック

画像の変換処理は、ブラウザのCanvas APIを使用しています。

private async convertToWebP(file: File): Promise<Blob> {
  return new Promise((resolve, reject) => {
    const img = new Image();
    const reader = new FileReader();

    reader.onload = (e) => {
      img.onload = () => {
        // 画像サイズの検証
        this.validateImageDimensions(img.width, img.height);

        // Canvasに描画
        const canvas = document.createElement('canvas');
        canvas.width = img.width;
        canvas.height = img.height;
        
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0);

        // WebPに変換
        canvas.toBlob(
          (blob) => {
            if (blob) {
              resolve(blob);
            } else {
              reject(new Error('Failed to convert to WebP'));
            }
          },
          'image/webp',
          this.settings.webpQuality
        );
      };

      img.src = e.target?.result as string;
    };

    reader.readAsDataURL(file);
  });
}

この実装のポイント:

  1. FileReaderでファイルをData URLとして読み込み
  2. Image要素にロード
  3. Canvasに描画
  4. canvas.toBlob()でWebP形式に変換(品質指定可能)

ファイル保存処理

Obsidian Vault APIを使用してファイルを保存します。

private async saveImage(blob: Blob, filename: string, view: MarkdownView): Promise<string> {
  // フォルダパスの決定
  let folder: string;
  
  if (this.settings.imageFolderLocation === 'current-folder') {
    const currentFile = view.file;
    const baseFolder = currentFile?.parent?.path || '';
    folder = baseFolder ? `${baseFolder}/${folderName}` : folderName;
  }
  // ... その他のロケーション処理

  // フォルダが存在しない場合は作成
  const folderExists = await this.app.vault.adapter.exists(folder);
  if (!folderExists) {
    await this.app.vault.createFolder(folder);
  }

  // 重複チェックと保存
  let filepath = `${folder}/${filename}`;
  let counter = 1;
  
  while (await this.app.vault.adapter.exists(filepath)) {
    const nameWithoutExt = filename.replace('.webp', '');
    filepath = `${folder}/${nameWithoutExt}-${counter}.webp`;
    counter++;
  }

  // ArrayBufferに変換して保存
  const arrayBuffer = await blob.arrayBuffer();
  await this.app.vault.createBinary(filepath, arrayBuffer);

  return filepath;
}

クリップボードイベントのハンドリング

Obsidianのエディタペーストイベントをインターセプトします。

async onload() {
  await this.loadSettings();

  this.registerEvent(
    this.app.workspace.on('editor-paste', this.handlePaste.bind(this))
  );

  this.addSettingTab(new PasteImageAsWebPSettingTab(this.app, this));
}

private async handlePaste(evt: ClipboardEvent, editor: Editor, view: MarkdownView) {
  const files = evt.clipboardData?.files;
  if (!files || files.length === 0) {
    return; // 画像がない場合は通常のペースト処理
  }

  const imageFiles = Array.from(files).filter(file =>
    file.type.startsWith('image/')
  );

  if (imageFiles.length === 0) {
    return;
  }

  evt.preventDefault(); // デフォルトのペースト動作を防ぐ

  for (const file of imageFiles) {
    await this.processImage(file, editor, view);
  }
}

インストール方法

コミュニティプラグインから(現在リリース申請中です)

プルリクエストが通ったら、コミュニティプラグインからインストールできます。
今しばらくお待ちくださいませ (+_+)

手動インストール

  1. GitHubリリースページからmain.jsmanifest.jsonstyles.cssをダウンロード
  2. Vaultのプラグインフォルダにpaste-image-as-webpフォルダを作成

  1. ダウンロードしたファイルを配置

  2. Obsidianを再起動してプラグインを有効化

使い方

  1. プラグインを有効化
  2. 設定画面で好みに合わせてカスタマイズ
  3. 画像をクリップボードにコピー
  4. Obsidianのエディタでペースト(Ctrl+V / Cmd+V)
  5. 自動的にWebP形式で保存され、エディタに挿入される

まとめ

「Paste Image as WebP」プラグインを使用することで、以下のメリットが得られます。

  1. Vaultのサイズ削減:画像ファイルサイズが20〜50%削減され、ストレージを節約
  2. シームレスな体験:通常のペースト操作と変わらない使い勝手
  3. 柔軟な設定:ファイル名、保存先、品質を自由にカスタマイズ可能
  4. セキュリティ:各種攻撃に対する防御機能を実装

画像を多用するObsidianユーザーにとって、このプラグインはVault管理の負担を大きく軽減できるツールとなるはずです。

GitHubでオープンソースとして公開していますので、バグ報告や機能要望はIssuesでお待ちしています。

リンク

補足

基本的な使い方

1. プラグインの有効化

  1. Obsidianの設定を開く(歯車アイコン)
  2. 左サイドバーから「コミュニティプラグイン」を選択
  3. 「Paste Image as WebP」を探して「有効化」をクリック

2. 画像の貼り付け手順

  1. 任意の画像をクリップボードにコピー
    • スクリーンショットを撮る(Windows: Win + Shift + S、Mac: Cmd + Shift + 4
    • ブラウザや画像ビューアーで画像を右クリック→「コピー」
    • 画像ファイルをコピー(Ctrl + C / Cmd + C
  2. Obsidianのエディタでペースト
    • Ctrl + V(Windows/Linux)
    • Cmd + V(Mac)
  3. 自動的に以下が実行される
    • 画像がWebP形式に変換
    • 指定したフォルダに保存
    • エディタに![[ファイル名.webp]]形式で挿入
    • 通知メッセージが表示される

設定項目の詳細

プラグインの設定画面は、「設定」→「コミュニティプラグイン」→「Paste Image as WebP」の歯車アイコンから開けます。

ファイル名設定(Filename format)

Filename format(ファイル名形式)

デフォルト値: Timestamp(タイムスタンプ)
選択肢:

  • Fixed name(固定名):毎回同じファイル名で保存
  • Timestamp(タイムスタンプ):日時ベースのファイル名
    設定方法:
    ドロップダウンメニューから選択します。

Fixed filename(固定ファイル名)

デフォルト値: image
適用条件: Filename formatが「Fixed name」の場合のみ表示
動作:

  • 設定した名前で保存される(例:image.webp
  • 同名ファイルが存在する場合、自動的に連番が追加される
    • 1枚目:image.webp
    • 2枚目:image-1.webp
    • 3枚目:image-2.webp
      設定方法:
  1. Filename formatを「Fixed name」に変更
  2. テキストフィールドに好きな名前を入力(拡張子は不要)
    使用例:
設定値: screenshot
結果: screenshot.webp, screenshot-1.webp, screenshot-2.webp...

Timestamp format(タイムスタンプフォーマット)

デフォルト値: YYYYMMDDHHmmss
適用条件: Filename formatが「Timestamp」の場合のみ表示
使用可能なプレースホルダー:

  • YYYY:年(4桁)例:2024
  • MM:月(2桁)例:01, 12
  • DD:日(2桁)例:01, 31
  • HH:時(2桁、24時間形式)例:00, 23
  • mm:分(2桁)例:00, 59
  • ss:秒(2桁)例:00, 59
    設定方法:
  1. Filename formatを「Timestamp」に変更
  2. テキストフィールドにフォーマットを入力
  3. リアルタイムでサンプルが表示される
    フォーマット例:
    | フォーマット | 出力例 | 用途 |
    |------------|--------|------|
    | YYYYMMDDHHmmss | 20241130143025.webp | デフォルト、秒単位で一意 |
    | YYYY-MM-DD_HHmmss | 2024-11-30_143025.webp | 読みやすい形式 |
    | YYYYMMDD_HH-mm-ss | 20241130_14-30-25.webp | 時刻を区切る |
    | YYYY-MM-DD | 2024-11-30.webp | 日付のみ(1日に1枚のみの場合) |
    | YYYYMMDD-HHmm | 20241130-1430.webp | 分単位(秒は不要な場合) |
    サンプル表示:
    設定画面でフォーマットを入力すると、下部に現在時刻でのサンプルが表示されます。
Sample: 20241130143025.webp

保存先設定

Image folder location(画像フォルダの場所)

デフォルト値: Same folder as current note(現在のノートと同じフォルダ)
選択肢:

  1. Same folder as current note:現在のノートと同じフォルダ
  2. Vault root:Vaultのルート
  3. Custom path:カスタムパス
    設定方法:
    ドロップダウンメニューから選択します。
    各選択肢の動作:
1. Same folder as current note(推奨)

現在編集中のノートと同じフォルダに、指定したサブフォルダを作成して保存します。
:

Vaultの構成:
├── Projects/
│   └── Project-A.md  ← 現在編集中
└── Daily/
    └── 2024-11-30.md
設定: Image folder name = "attachments"
保存先:
Projects/attachments/20241130143025.webp

メリット:

  • ノートと画像が近くに配置される
  • プロジェクトごとに画像を整理できる
  • ノートを移動しても相対パスが維持される

2. Vault root

Vaultのルートに、指定したフォルダを作成して保存します。
:

Vaultの構成:
├── Projects/
│   └── Project-A.md  ← 現在編集中
└── attachments/      ← ここに保存される
    └── 20241130143025.webp

メリット:

  • すべての画像が1箇所に集約される
  • 画像の一括管理が容易
    デメリット:
  • ノート数が多いと画像フォルダが巨大になる

3. Custom path

任意のパスを指定して保存します。
:

設定: Custom folder path = "resources/images"
保存先:
resources/images/20241130143025.webp

メリット:

  • 完全に自由なフォルダ構造を実現
  • 複数レベルのフォルダを指定可能

Image folder name(画像フォルダ名)

デフォルト値: attachments
適用条件: Image folder locationが「Custom path」以外の場合に表示
設定方法:
テキストフィールドにフォルダ名を入力します。
動作:

  • フォルダが存在しない場合、自動的に作成される
  • 既存のフォルダがある場合、そこに保存される
    :
設定値: images
結果: 
- Same folder as current noteの場合: ノートのフォルダ/images/
- Vault rootの場合: Vaultルート/images/

Custom folder path(カスタムフォルダパス)

デフォルト値: project/images
適用条件: Image folder locationが「Custom path」の場合のみ表示
設定方法:

  1. Image folder locationを「Custom path」に変更
  2. テキストフィールドに完全なパスを入力
    パス指定の規則:
  • Vaultルートからの相対パス
  • /(スラッシュ)で区切る
  • 先頭や末尾のスラッシュは不要
    :
設定値: resources/screenshots
結果: Vaultルート/resources/screenshots/
設定値: work/projects/project-a/assets
結果: Vaultルート/work/projects/project-a/assets/

サンプル表示:
設定画面の下部に保存先のプレビューが表示されます。

Images will be saved to: project/images/

画質設定

Image quality (WebP)(画像品質)

デフォルト値: 0.85
範囲: 0.1〜1.0(スライダーで調整、0.05刻み)
説明:

  • 1.0:最高品質(ファイルサイズが最大)
  • 0.85:推奨値(品質とサイズのバランス)
  • 0.5〜0.7:低品質(ファイルサイズ重視)
    設定方法:
    スライダーをドラッグして調整します。リアルタイムで値が表示されます。
    品質とファイルサイズの目安:
品質設定 ファイルサイズ 画質 推奨用途
1.0 100% 最高 写真の保管、印刷用
0.9 約80% 非常に高い 高品質が必要な画像
0.85 約70% 高い 一般的な用途(推奨)
0.8 約60% 高い スクリーンショット
0.7 約50% 中程度 図表、ダイアグラム
0.5 約30% 低い サムネイル、下書き
実例(1920×1080のスクリーンショット):
PNG形式: 約2.5MB
品質1.0: 約1.5MB(40%削減)
品質0.85: 約800KB(68%削減)← デフォルト
品質0.7: 約500KB(80%削減)

セキュリティ設定

Maximum image size(最大画像サイズ)

デフォルト値: 16777216(ピクセル)
説明:
画像の最大ピクセル数(幅 × 高さ)を制限します。
計算例:

  • 16,777,216ピクセル = 4096 × 4096
  • 4,194,304ピクセル = 2048 × 2048
  • 67,108,864ピクセル = 8192 × 8192
    設定方法:
    テキストフィールドに数値を入力します。
    推奨値:
用途 推奨値 サイズ例
一般的な用途 16,777,216 4096×4096
高セキュリティ環境 4,194,304 2048×2048
パワーユーザー 67,108,864 8192×8192
目的:
巨大な画像によるメモリ枯渇(DoS攻撃)を防ぎます。
エラー時の挙動:
制限を超える画像をペーストすると、エラーメッセージが表示され、画像は保存されません。
Image dimensions exceed maximum allowed size

Maximum file size (MB)(最大ファイルサイズ)

デフォルト値: 10 MB
説明:
ペースト可能な画像ファイルの最大サイズをMB単位で制限します。
設定方法:
テキストフィールドに数値を入力します(小数点も使用可能)。
推奨値:

用途 推奨値
一般的な用途 10 MB
高セキュリティ環境 5 MB
パワーユーザー 50 MB
:
設定値: 5
結果: 5MB以下の画像のみペースト可能
設定値: 2.5
結果: 2.5MB以下の画像のみペースト可能

エラー時の挙動:
制限を超える画像をペーストすると、エラーメッセージが表示されます。

Image file size exceeds 10MB limit

実践的な設定例

例1: 日記用の設定(デフォルト推奨)

Filename format: Timestamp
Timestamp format: YYYYMMDDHHmmss
Image folder location: Same folder as current note
Image folder name: attachments
Image quality: 0.85
Maximum image size: 16777216
Maximum file size: 10 MB

動作:

Vaultの構成:
├── Daily/
│   ├── 2024-11-30.md  ← 編集中
│   └── attachments/
│       └── 20241130143025.webp  ← 保存される

例2: プロジェクト管理用の設定

Filename format: Fixed name
Fixed filename: screenshot
Image folder location: Same folder as current note
Image folder name: images
Image quality: 0.8
Maximum image size: 16777216
Maximum file size: 10 MB

動作:

Vaultの構成:
├── Projects/
│   ├── ProjectA/
│   │   ├── overview.md  ← 編集中
│   │   └── images/
│   │       ├── screenshot.webp
│   │       ├── screenshot-1.webp
│   │       └── screenshot-2.webp

例3: 一元管理用の設定

Filename format: Timestamp
Timestamp format: YYYY-MM-DD_HHmmss
Image folder location: Vault root
Image folder name: assets
Image quality: 0.85
Maximum image size: 16777216
Maximum file size: 10 MB

動作:

Vaultの構成:
├── assets/
│   ├── 2024-11-30_143025.webp
│   ├── 2024-11-30_150312.webp
│   └── 2024-11-30_163540.webp
├── Projects/
│   └── overview.md  ← どこから貼っても assets/ に保存
└── Daily/
    └── 2024-11-30.md

例4: 高品質写真保存用

Filename format: Timestamp
Timestamp format: YYYY-MM-DD_HHmmss
Image folder location: Custom path
Custom folder path: media/photos
Image quality: 0.95
Maximum image size: 67108864
Maximum file size: 50 MB

用途: 写真をアーカイブとして保存する場合

トラブルシューティング

画像がペーストされない

確認事項:

  1. プラグインが有効化されているか
  2. クリップボードに画像データがあるか
  3. ファイルサイズや画像サイズが制限内か
    対処法:
  • コンソール(Ctrl + Shift + I)でエラーを確認
  • セキュリティ設定の制限を一時的に緩和して確認

フォルダが作成されない

原因:
親フォルダが存在しない場合、自動作成されません。
対処法:

  1. 手動で親フォルダを作成
  2. または、シンプルなフォルダ構造に変更
    :
設定: Custom folder path = "projects/work/screenshots"
親フォルダ "projects/work" が存在しない場合 → エラー
対処: 先に "projects/work" フォルダを手動で作成

同名ファイルが増え続ける

原因:
Fixed name形式で連番が増え続けている。
対処法:

  1. Timestamp形式に変更
  2. または、定期的に古い画像を整理

まとめ

「Paste Image as WebP」プラグインの設定は、用途に応じて柔軟にカスタマイズできます。

推奨設定:

  • 一般ユーザー:デフォルト設定のまま
  • こだわり派:Timestamp formatを読みやすい形式に
  • 大量の画像を扱う:品質を0.7〜0.8に下げてファイルサイズを削減
    自分の使い方に合わせて、最適な設定を見つけてください。

Discussion