Markdown Image Renderer for AI Studio

通称:スタ画、英名「MIR」

目的:AI Studioのプロンプト画面でMarkdown形式の画像リンクを実際の画像として表示します

使用手順:①Tampermonkeyのプラグインを インストール
②「MIRインストール」を押して、スクリプトインストールを行う(またはTampermonkeyで、以下のコードを保存)
Google Studio AIを起動して画像出る系のプロンプトでプレイ
※下部のスタ画ライブラリでローカルで画像管理も可能になりました

うまくいくと以下の様にでます
C

詳しくは下部

// ==UserScript==
// @name         Markdown Image Renderer for Google AI Studio
// @namespace    https://rentry.co/3bnuvgwu
// @license      MIT
// @version      5.7
// @description  AI StudioでMarkdown画像を安定表示する拡張版localhost画像の取得を改善
// @author       ForeverPWA
// @match        *://aistudio.google.com/*
// @grant        GM_xmlhttpRequest
// @connect      *
// ==/UserScript==

(function () {
    'use strict';

    const LOG_PREFIX = "MIR:";
    console.log(LOG_PREFIX, "v5.7 started");

    const DB_NAME = 'ImageManagerDB';
    const DB_VERSION = 1;

    let imageCache = {};
    let cacheReady = false;
    const blobUrlCache = {};
    let previewWindow = null;

    function openInPreview(url) {
        try {
            if (previewWindow && !previewWindow.closed) {
                previewWindow.location.replace(url);
                previewWindow.focus();
            } else {
                previewWindow = window.open(url, 'imagePreviewWindow');
            }
        } catch (e) {
            previewWindow = window.open(url, 'imagePreviewWindow');
        }
    }

    function openDB() {
        return new Promise((resolve, reject) => {
            const req = indexedDB.open(DB_NAME, DB_VERSION);
            req.onerror = () => reject(req.error);
            req.onsuccess = () => resolve(req.result);
        });
    }

    async function loadImageCache() {
        try {
            const db = await openDB();

            const characters = await getAll(db, 'characters');
            const outfits = await getAll(db, 'outfits');
            const images = await getAll(db, 'characterImages');

            db.close();

            const charMap = {};
            characters.forEach(c => { charMap[c.id] = c.name; });

            const outfitMap = {};
            outfits.forEach(o => {
                outfitMap[o.id] = {
                    charName: charMap[o.characterId] || 'unknown',
                    outfitName: o.name
                };
            });

            images.forEach(img => {
                const outfit = outfitMap[img.outfitId];
                if (outfit) {
                    const key = `:character/${outfit.charName}/${outfit.outfitName}/${img.name}`;
                    imageCache[key] = {
                        data: img.data,
                        mimeType: img.mimeType
                    };
                }
            });

            cacheReady = true;
            console.log(LOG_PREFIX, `Loaded ${images.length} images, cache keys:`, Object.keys(imageCache).slice(0, 5));
        } catch (e) {
            console.warn(LOG_PREFIX, "IndexedDB error:", e);
            cacheReady = true;
        }
    }

    function getAll(db, storeName) {
        return new Promise((resolve, reject) => {
            const tx = db.transaction(storeName, 'readonly');
            const store = tx.objectStore(storeName);
            const req = store.getAll();
            req.onsuccess = () => resolve(req.result || []);
            req.onerror = () => reject(req.error);
        });
    }

    function getBlobUrlFromDataUrl(cacheKey, dataUrl) {
        if (blobUrlCache[cacheKey]) return blobUrlCache[cacheKey];

        const [header, base64Data] = dataUrl.split(',');
        const mimeType = header.match(/data:([^;]+)/)?.[1] || 'image/png';
        const byteCharacters = atob(base64Data);
        const byteNumbers = new Array(byteCharacters.length);
        for (let i = 0; i < byteCharacters.length; i++) {
            byteNumbers[i] = byteCharacters.charCodeAt(i);
        }
        const byteArray = new Uint8Array(byteNumbers);
        const blob = new Blob([byteArray], { type: mimeType });
        const blobUrl = URL.createObjectURL(blob);
        blobUrlCache[cacheKey] = blobUrl;
        return blobUrl;
    }

    function isLocalhostUrl(url) {
        try {
            const parsed = new URL(url, window.location.href);
            return parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
        } catch (e) {
            return false;
        }
    }

    function isSameOriginUrl(url) {
        try {
            const parsed = new URL(url, window.location.href);
            return parsed.origin === window.location.origin;
        } catch (e) {
            return url.startsWith('/');
        }
    }

    function fetchViaWindowFetch(url, callback) {
        fetch(url, { mode: 'cors', credentials: 'omit' })
            .then(r => {
                if (!r.ok) {
                    throw new Error(`HTTP ${r.status}`);
                }
                return r.blob();
            })
            .then(callback)
            .catch((error) => {
                console.warn(LOG_PREFIX, `fetch failed for ${url}:`, error);
                callback(null);
            });
    }

    function fetchImageAsBlob(url, callback) {
        if (isSameOriginUrl(url) || url.startsWith('/')) {
            fetchViaWindowFetch(url, callback);
            return;
        }

        if (isLocalhostUrl(url)) {
            console.log(LOG_PREFIX, `localhost fetch path: ${url}`);
            fetchViaWindowFetch(url, (blob) => {
                if (blob) {
                    callback(blob);
                    return;
                }

                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    responseType: 'blob',
                    onload: (r) => {
                        if (r.status >= 200 && r.status < 300) {
                            callback(r.response);
                        } else {
                            console.warn(LOG_PREFIX, `GM_xmlhttpRequest failed for ${url}: HTTP ${r.status}`);
                            callback(null);
                        }
                    },
                    onerror: (error) => {
                        console.warn(LOG_PREFIX, `GM_xmlhttpRequest network error for ${url}:`, error);
                        callback(null);
                    }
                });
            });
            return;
        }

        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            responseType: 'blob',
            onload: (r) => {
                if (r.status >= 200 && r.status < 300) {
                    callback(r.response);
                } else {
                    console.warn(LOG_PREFIX, `GM_xmlhttpRequest failed for ${url}: HTTP ${r.status}`);
                    callback(null);
                }
            },
            onerror: (error) => {
                console.warn(LOG_PREFIX, `GM_xmlhttpRequest network error for ${url}:`, error);
                callback(null);
            }
        });
    }

    function processPreElement(pre) {
        if (pre.dataset.mir) return;

        const text = pre.textContent || '';
        const match = text.match(/!\[([^\]]*)\]\(([^)]+)\)/);
        if (!match) return;

        const [, alt, src] = match;
        pre.dataset.mir = '1';

        if (src.startsWith(':character/')) {
            if (!cacheReady) return;

            const pathNoExt = src.replace(/\.[^/.]+$/, '');
            const imgData = imageCache[pathNoExt] || imageCache[src];

            if (imgData) {
                console.log(LOG_PREFIX, `Found: ${src}`);

                const img = document.createElement('img');
                img.src = imgData.data;
                img.alt = alt || src;
                img.style.cssText = "max-width:100%;height:auto;display:block;margin:10px 0;border-radius:8px;cursor:pointer;";
                img.title = src;
                img.onclick = (e) => {
                    e.preventDefault();
                    try {
                        if (imgData.data.startsWith('data:')) {
                            const blobUrl = getBlobUrlFromDataUrl(src, imgData.data);
                            openInPreview(blobUrl);
                        } else {
                            openInPreview(imgData.data);
                        }
                    } catch (err) {
                        console.error(LOG_PREFIX, 'preview open failed:', err);
                    }
                };

                pre.style.display = 'none';
                pre.parentNode?.insertBefore(img, pre.nextSibling);
            } else {
                console.log(LOG_PREFIX, `Not found: ${src} (keys: ${Object.keys(imageCache).length})`);
            }
            return;
        }

        console.log(LOG_PREFIX, `Fetch: ${src}`);

        const img = document.createElement('img');
        img.alt = alt || 'Loading...';
        img.style.cssText = "max-width:100%;height:auto;display:block;margin:10px 0;border-radius:8px;background:#f0f0f0;min-height:50px;cursor:pointer;";
        img.title = src;

        let previewSrc = src;
        img.onclick = (e) => {
            e.preventDefault();
            openInPreview(previewSrc);
        };

        pre.style.display = 'none';
        pre.parentNode?.insertBefore(img, pre.nextSibling);

        fetchImageAsBlob(src, (blob) => {
            if (blob) {
                const objectUrl = URL.createObjectURL(blob);
                previewSrc = objectUrl;
                img.src = objectUrl;
                img.alt = alt;
            } else {
                img.alt = `[Failed] ${alt}`;
                img.style.border = "2px dashed #d93025";
                console.warn(LOG_PREFIX, `Image load failed: ${src}`);
            }
        });
    }

    function scan() {
        document.querySelectorAll('ms-chat-turn pre, .prompt-textarea pre').forEach(processPreElement);
    }

    let timer = null;
    new MutationObserver(() => {
        clearTimeout(timer);
        timer = setTimeout(scan, 300);
    }).observe(document.body, { childList: true, subtree: true });

    loadImageCache().then(() => {
        setTimeout(scan, 500);
    });
})();

1. Tampermonkeyって何?

Tampermonkeyは、一言でいうと「Webサイトを自分好みに改造できるGoogle Chromeの拡張機能」です。

例えば、以下のようなことができます。

  • 見た目を変更: 文字の大きさを変えたり、不要な広告を非表示にしたりできます。
  • 機能を追加: YouTubeにダウンロードボタンを追加したり、SNSサイトに便利な機能を追加したりできます。
  • 作業を自動化: 定型的なフォーム入力を自動化したりできます。

これらは「ユーザースクリプト」と呼ばれる小さなプログラムをTampermonkeyに追加することで実現します。自分でプログラムを書けなくても、他の人が作成して公開しているスクリプトをインストールするだけで、様々な恩恵を受けられます。

2. Tampermonkeyのインストール(超かんたん!)

まずは、TampermonkeyをGoogle Chromeに追加しましょう。

  1. Chromeウェブストアにアクセス: こちらのリンクからTampermonkeyのページを開きます。
  2. 「Chromeに追加」をクリック: 右上にある青い「Chromeに追加」ボタンをクリックします。
  3. 拡張機能を追加: 確認画面が表示されたら「拡張機能を追加」をクリックします。

これだけでインストールは完了です。 ブラウザの右上に、黒い四角に丸が2つ描かれたTampermonkeyのアイコンが表示されていれば成功です。

3. スクリプトを探してインストールしてみよう

Tampermonkeyの真価は、便利なユーザースクリプトを追加することにあります。スクリプトは、専門のサイトで探すのが一般的です。

  • Greasy Fork (グリーシーフォーク): 最も有名で、たくさんのスクリプトが公開されているサイトです。

Greasy Forkでスクリプトをインストールする手順:

  1. サイトにアクセス: Greasy Forkにアクセスします。
  2. スクリプトを探す: サイト内検索で、自分が使っているWebサイト名(例:「YouTube」)や、やりたいこと(例:「広告非表示」)などで検索します。
  3. インストール: 気に入ったスクリプトが見つかったら、そのページを開き、「スクリプトをインストール」という緑色のボタンをクリックします。
  4. 確認画面: Tampermonkeyの画面に切り替わり、スクリプトの内容が表示されます。内容を確認し、左上にある「インストール」ボタンをクリックすれば完了です。

4. 開発者モードの使い方(自作・改造に挑戦!)

ここからは、自分でスクリプトを作成したり、既存のスクリプトを改造したりするための「開発者モード」の使い方です。

なぜ開発者モードが必要?

最近のChromeの仕様変更により、Tampermonkeyでスクリプトを動作させるには「開発者モード」を有効にすることが必須となりました。 これはセキュリティ上の理由によるものです。

開発者モードを有効にする手順

  1. 拡張機能の管理画面を開く:
    • Chromeの右上にあるパズルのピースのような「拡張機能」アイコンをクリックします。
    • メニューの下部にある「拡張機能を管理」を選択します。
  2. 開発者モードをONにする:
    • 拡張機能一覧ページの右上にある「デベロッパー モード」(または「開発者モード」)のスイッチをクリックしてONにします。
  3. ユーザースクリプトをONにする:
    • Tampermonkeyの詳細を押す
      C
    • 「ユーザースクリプトを許可する」をONにする
      C
    • 「URLのアクセスを許可する」をONにする→上部にあった「MIRインストール」が可能になります
      C

新しいスクリプトを作成する方法

  1. Tampermonkeyアイコンをクリック: ブラウザ右上のTampermonkeyアイコンをクリックします。
  2. 「新規スクリプトを追加」を選択: 表示されたメニューから「新規スクリプトを追加」をクリックします。
  3. エディタ画面が開く: 新しいタブで、スクリプトを編集するためのエディタ画面が開きます。
  4. コードを記述: 最初から入っているテンプレートに沿って、JavaScriptのコードを書いていきます。
    • // @name: スクリプトの名前
    • // @match: スクリプトをどのWebサイトで実行するかを指定します。 例えば、https://www.google.com/*と書くと、Googleのページでのみ実行されます。
    • // Your code here... の下に、実行したい処理をJavaScriptで記述します。
  5. 保存: コードを書き終えたら、エディタ画面の「ファイル」メニューから「保存」をクリックします。

これで、あなたの作成したスクリプトが有効になり、指定したサイトを開くと自動的に実行されるようになります。もしうまく動かない場合は、開発者ツールのコンソールでエラーを確認してみましょう。

5. 注意点

  • AIにスクリプトの内容を確認する: コードをAIに聞いて安全か確認してみよう
  • 信頼できるスクリプトを: スクリプトは便利な反面、悪意のあるコードが含まれている可能性もゼロではありません。出所が不明なスクリプトや、内容がよく分からないスクリプトのインストールは避けましょう。
  • 定期的なアップデート: Tampermonkeyやインストールしたスクリプトは、自動で更新される設定にしておくと、セキュリティ面でも安心です。

Tampermonkeyを使いこなせば、普段のWebブラウジングが格段に快適になります。ぜひ、色々なスクリプトを試したり、簡単なスクリプトの自作に挑戦してみてください。

うまくいかない!

ブラウザの再起動か、「画像は()括弧でくくって」とチャットで打つと直る事もあり

こちらの画像は以下の提供でお送りしました。ありがとうございます。
個人的シコキャラ置き場様
https://rentry.co/ar6e9t2n

関連ツール

スタ画ライブラリ

当該ツールで表示する画像を管理できます
https://rentry.co/78hcq7c7/

その他のツール

URL自動デコーダー

通称:自デコ
https://rentry.co/3hb6piip

AI Studio以外のAIモデルでも画像出力したい!(Gemini, ChatGPT, Grok用)

Markdown Image Renderer for Gemini, ChatGPT, Grok
通常:MIRG
https://rentry.co/kuzpbema

AI Studioで自動音声読み上げ(VOICEVOX)が欲しい!

通称:スタ自ボ
https://rentry.co/x9fw82o3

Wikiのプロンプトまとめ等をみながら、プロンプトだけを即コピーしたい人用に作成しました

通称:即コピ
https://rentry.co/8772bcnh

Edit

Pub: 18 Oct 2025 18:23 UTC

Edit: 24 May 2026 15:18 UTC

Views: 2505