[JavaScript] 20 Adobe XDのアイテムデータを取得するプラグイン作成 その6 Pythonスクリプトとの統合 / GPT-4 Turbo

[Mac M2 Pro 12CPU, Ventura 13.6]

ChatGPTがGPT-4 Turboになってトークン数に余裕ができたので、早速仕事をしてもらいました。プラグインのmain.jsにtxtTojson.pyの内容を取り込みました。

自力でもある程度プログラミングできるレベルであれば、ChatGPTによる恩恵は絶大だと思います。

const fs = require("uxp").storage.localFileSystem;

async function myCommand(selection) {
    let items_list = [];
    selection.items.forEach(function (value, index) {
        // ここでPythonスクリプトの処理に相当するデータ抽出を行う
        const name = value.name;
        const bounds = value.globalBounds; // globalBoundsオブジェクトを取得
        const x = bounds.x;
        const y = bounds.y;
        const w = bounds.width; // globalBoundsからwidthを取得
        const h = bounds.height; // globalBoundsからheightを取得
        items_list.push({ name, bounds: [x, y, w, h] });
    });

    // JSONオブジェクトを作成
    const json_obj = {};
    items_list.forEach(item => {
        json_obj[item.name] = item.bounds;
    });

    // JSON文字列に変換
    const json_str = JSON.stringify(json_obj, null, 2);

    // ファイルを保存する
    const file = await fs.getFileForSaving("output.json", { types: ["json"] });
    await file.write(json_str);
}

module.exports = {
    commands: {
        GetItemXY: myCommand
    }
};
AdobeXDプラグインを使って2つの方法でアイテムデータを取得し、比較しています
1.プラグインのmain.jsでアイテムデータをtxtファイル化し、txtTojson.pyでjsonファイルに変換する
2.プラグインのmain.jsでアイテムデータをjsonファイル化する

1の方法では成功しますが、2の方法ではデータの欠損が生じます。

以下にそれぞれのスクリプトを記述しますので、これらを参考に2の方法のmain.jsを修正してください

1の方法
-- main.js
const fs = require("uxp").storage.localFileSystem;

async function myCommand(selection) {
    let items_list = "";
    var num = 1;
    selection.items.forEach(function(value){
        if (num < selection.items.length){
            items_list += value + ";";
        } else {
            items_list += value;
        }
        num += 1;
    });

    // ファイルを保存する
    const file = await fs.getFileForSaving("output.txt", { types: ["txt"] });
    await file.write(items_list);
}

module.exports = {
    commands: {
        GetItemXY: myCommand
    }
};
--

-- txtTojson.py
import json

with open("items.txt", "r") as tf:
    items_list = tf.read().replace("\n","").split(';')
    
# print(items_list)
# print(len(items_list))

# listからデータ抽出し、JSON文字列作成
json_str = ""
num = 0
for item in items_list:
    name = item.split("'")[1]
    print(name + "\n")
    
    xy = (item.split("global X,Y:")[1]).split("parent")[0]
    x = xy.split(",")[0]
    y = xy.split(",")[1]
    print(x + "\n")
    print(y + "\n")
    
    wh = (item.split("{")[1]).split("global")[0]
    w = (wh.split("width:")[1]).split(", height")[0]
    h = wh.split("height:")[1]
    print(w + "\n")
    print(h + "\n")
    
    if num == 0:
        json_str += "{\"" + name + "\"" + ":[" + x + ", " + y + ", " + w + ", " + h + "],\n"
    elif num < len(items_list) -1:
        json_str += "\"" + name + "\"" + ":[" + x + ", " + y + ", " + w + ", " + h + "],\n"
    else:
        json_str += "\"" + name + "\"" + ":[" + x + ", " + y + ", " + w + ", " + h + "]}"

    print(json_str + "\n")
    
    num += 1

# JSONファイル作成
file = open('items.json', mode='w')
file.write(json_str.replace(" ",""))
file.close()
--

-- items.json
{"input":{"x":20,"y":23,"w":14,"h":18},
"長方形11":{"x":6,"y":50,"w":30,"h":30},
"inputLine":{"x":40,"y":30,"w":370,"h":130},
"showBtn":{"x":40,"y":10,"w":370,"h":20},
"showBtn":{"x":20,"y":165,"w":160,"h":30},
"長方形7":{"x":20,"y":200,"w":460,"h":320},
"Codeカウント":{"x":196,"y":165,"w":40,"h":30},
"Codeカウント":{"x":252,"y":165,"w":60,"h":30},
"showBtn":{"x":327,"y":165,"w":28,"h":30},
"Codeカウント":{"x":340,"y":525,"w":40,"h":20},
"長方形9":{"x":20,"y":550,"w":460,"h":82},
"showBtn":{"x":370,"y":165,"w":28,"h":30},
"showBtn":{"x":387,"y":525,"w":26,"h":20}}
--

2の方法
-- main.js
const fs = require("uxp").storage.localFileSystem;

async function myCommand(selection) {
    let items_list = [];
    selection.items.forEach(function (value) {
        // ここでPythonスクリプトの処理に相当するデータ抽出を行う
        const name = value.name;
        const x = value.globalBounds.x;
        const y = value.globalBounds.y;
        const w = value.width;
        const h = value.height;
        items_list.push({ name, bounds: [x, y, w, h] });
    });

    // JSONオブジェクトを作成
    const json_obj = {};
    items_list.forEach(item => {
        json_obj[item.name] = item.bounds;
    });

    // JSON文字列に変換
    const json_str = JSON.stringify(json_obj, null, 2);

    // ファイルを保存する
    const file = await fs.getFileForSaving("output.json", { types: ["json"] });
    await file.write(json_str);
}

module.exports = {
    commands: {
        GetItemXY: myCommand
    }
};
--

-- output.json(データが欠損している)
{
  "長方形 7": [
    20,
    200,
    460,
    320
  ],
  "showBtn": [
    327,
    165,
    null,
    null
  ],
  "長方形 9": [
    20,
    550,
    460,
    82
  ],
  "Codeカウント": [
    252,
    165,
    null,
    null
  ]
}
--
2の方法でデータの欠損が生じている原因は、同じ名前のアイテムが複数存在する場合、JSONオブジェクトにおいて後から追加されたアイテムで以前のアイテムのデータが上書きされてしまうためです。JSONオブジェクトのキーは一意でなければならないため、同じ名前のアイテムがあると問題が発生します。

この問題を解決するためには、アイテムの名前が重複しないように一意のキーを生成するか、アイテムのリストを配列としてJSONに保存する必要があります。以下に修正したスクリプトを示します。

```javascript
const fs = require("uxp").storage.localFileSystem;

async function myCommand(selection) {
    let items_list = [];
    selection.items.forEach(function (value, index) {
        // ここでPythonスクリプトの処理に相当するデータ抽出を行う
        const name = value.name;
        const x = value.globalBounds.x;
        const y = value.globalBounds.y;
        const w = value.width;
        const h = value.height;
        // 同じ名前のアイテムがある場合に備えて、インデックスを名前に追加
        const uniqueName = `${name}_${index}`;
        items_list.push({ name: uniqueName, bounds: [x, y, w, h] });
    });

    // JSONオブジェクトを作成
    const json_obj = {};
    items_list.forEach(item => {
        json_obj[item.name] = item.bounds;
    });

    // JSON文字列に変換
    const json_str = JSON.stringify(json_obj, null, 2);

    // ファイルを保存する
    const file = await fs.getFileForSaving("output.json", { types: ["json"] });
    await file.write(json_str);
}

module.exports = {
    commands: {
        GetItemXY: myCommand
    }
};
```

この修正により、各アイテムに一意の名前が付けられ、JSONオブジェクトに保存される際にデータの上書きが発生しなくなります。ただし、この方法では元のアイテム名とインデックスが結合された形で保存されるため、元の名前だけを知りたい場合は後処理が必要になるかもしれません。それを避けるためには、アイテムを配列として保存する方法も考えられます。

[JavaScript] 19 Adobe XDのアイテムデータを取得するプラグイン作成 その5 作業手順 / UXP for Adobe XD / Pythonでjson化

[Mac M2 Pro 12CPU, Ventura 13.6]

作業手順をまとめておきます。

1.Adobe XDのデザインタブでXY座標を取得したい項目を選択する

2. メニューのプラグインからItemXYGetterを選択する。出力ファイルを指定して実行する。

3.出力されたtxtファイルをtxtTojson.pyで処理し、jsonファイルにする。

時間があればJavaScriptとPythonのスクリプトを統合し、JavaScriptのプラグインだけで処理できるようにしたいです。

※ ItemXYGetterのjsスクリプトの内容等は過去記事で紹介しています。画面左の検索窓で探してみてください。

Pythonスクリプトでjsonファイルに変換

[JavaScript] 18 Adobe XDのアイテムデータを取得するプラグイン作成 その4 テキスト出力 / UXP for Adobe XD

[Mac M2 Pro 12CPU, macOS Ventura 13.5.2]

Adobe XDでitemの座標とサイズを出力するプラグインを作成しています。

これまでは開発者コンソールに出力させてからコピー&ペーストしてテキストファイルを作成していました。

UXP for Adobe XDというプラットフォームを使うとファイル選択画面を表示させてテキストファイルに出力させることが出来ます。

ChatGPT(gpt-4)がこのプラットフォームの存在を教えてくれました。gpt-3.5ではAdobe XDのような商業アプリに関する情報はほとんど扱っていません。

一部有識者がgpt-4の性能低下を明らかにしていますが、それは高度な計算を大量に実行する場合の話であって、一般的な用途では変わらず優秀だと言えるでしょう。

const fs = require("uxp").storage.localFileSystem;

async function myCommand(selection) {
    let items_list = "";
    var num = 1;
    selection.items.forEach(function(value){
        if (num < selection.items.length){
            items_list += value + ";";
        } else {
            items_list += value;
        }
        num += 1;
    });

    // txtファイルとして保存する
    const file = await fs.getFileForSaving("output.txt", { types: ["txt"] });
    await file.write(items_list);
}

module.exports = {
    commands: {
        GetItemXY: myCommand
    }
};
前回の記事

[JavaScript] 17 Chrome拡張機能をSafariへ移植

[Mac M2 Pro 12CPU, macOS Ventura 13.5]

YouTubeをカスタマイズする拡張機能 own-youtubeをSafariへ移植しました。ソースコードはGitHubから入手しました。

移植と言ってもそんな大層な内容ではなく、Xcode上でChrome版をコピーしただけです。Finder上でファイルコピーしてもxcodeprojファイルに反映されないため移植は出来ません。

iPadOSでは正常に動作しましたが、iOSでは検索窓だけの初期画面にはなりませんでした。

この拡張機能を導入してから安易に動画を再生しなくなり、時間を無駄に消費することもなくなりました。

Safari Extension Appの空プロジェクトを作成してからChrome版をコピーする

[C++] 346 BBS閲覧アプリの製作 その30 レス番号へスクロール JavaScript

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

レスアンカー先へクリックでスクロールできるようにしました。

意外と手間取りました。HTML埋め込みのJavaScriptではtopプロパティで絶対Y座標が取得できるのですが、JavaScriptファイルでは相対Y座標でした。

なのでwindow.scrollYで現在位置を把握し、これにアンカー先の相対Y座標を加算する形になります。

// クリックしたレス番号までスクロール
function moveToRes(element){
    var resNum = element.innerText.replace(/>>/g, '');
    console.log("resNum: " + resNum);

    var targetElement = document.getElementById(resNum);
    var rect = targetElement.getBoundingClientRect();
    var yTo = rect.top;
    console.log("yTo = " + yTo);

    var currentY = window.scrollY;
    var scrollToY = currentY + yTo;
    console.log("scrollToY = " + scrollToY);

    scrollTo(0, scrollToY);
}

[JavaScript] 16 変数をファイルにして自動ダウンロード Blobオブジェクト

[M1 Mac, MacOS Ventura 13.3.1]

JavaScript内で生成した変数を他のプログラムで使えるようにするのは、C++ユーザーにとって最初はなかなか難易度が高いです。セキュリティ対策のため、JavaScript(非Node.js)はローカルファイルを作成して保存することができません。

今回はBlobオブジェクトを使ってファイル化しダウンロードするようにしました。

ChromeやEdgeでは自動的にダウンロードされ、Safariでは許可ボタンのクリックを経てダウンロードされます。つまりSafariでは自動化不可です。

ただ、モーダルダイアログにある許可ボタンの座標が分かれば、最近紹介したmacOSのApplication Servicesを使って自動クリックできそうな感じではあります。ややこしそうなので今回はやめておきます。

製作中のBBSブラウザで使用しているFl_WebViewはダウンロード機能を実装していないため、変数をファイル化してもダウンロードはできません。

// クリックしたaタグを含むpタグのIDを取得、ファイル化してダウンロード
function getID(element) {
    let id = element.closest('p').id;
    console.log("id = " + id);
  
    let data = new Blob([id], { type: 'text/plain' });
    let link = document.createElement('a');
    link.href = URL.createObjectURL(data);
    link.download = 'test.txt';
    link.click();
}

[C++] 341 BBS閲覧アプリの製作 その27 リンク画像をポップアップ表示 JavaScript

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

画像のURLをマウスオーバーするとぼかし画像がポップアップ表示され、クリックすると加工無しの画像がポップアップ表示されるようにしました。

ここが山場になるかと構えていましたが、思っていたよりもあっさりでした。それでもChatGPTを駆使しながら4時間程度掛かっています。

最も手こずったのはクリックして指定サイズでポップアップさせるところでしょうか。どうしても普通にリンクを踏む形になって大きいサイズでの埋め込み表示になってしまいました。結局、event.preventDefault()でデフォルトのリンク処理をスキップして解決しました。

たまにポップアップの後に影が残ったりしますが、後日対応することにします。

マウスオーバーでぼかし画像表示
クリックで元の画像表示

<html lang="ja">
<HEAD>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="styles.css" />
<script type="text/javascript" src="function.js"></script>
<STYLE TYPE="text/css">
    .popup-image {
        position: absolute;
        top: 0;
        left: 300px;
        width: 300px;
        height: auto;
        display: none;
        filter: blur(10px); // ぼかし加工
      }
    .popup-image2 {
        position: absolute;
        top: 0;
        left: 300px;
        width: 500px;
        height: auto;
        display: none;
    }
</STYLE>
</HEAD>
<BODY bgcolor="#ffffff">

<p id ='1'><font size="2" color="#008080">1 : テスト<br></font>
<font size="2" color="#000080">2023/07/22(土) 10:23:46.76 ID:xxx<br></font>
<font size="2" color="#000B00"> テスト <br> <a href="画像のURL" 
rel="noopener noreferrer" onmouseover="showImage(this,event);" onmouseout="hideImage();" 
onclick="showImage2(this,event);">画像のURL</a>
</font></p>

<script>
    // ぼかしあり画像表示
    function showImage(element,event) {
        var imageSrc = element.getAttribute("href");
        var imageElement = document.createElement("img");
        imageElement.src = imageSrc;
        imageElement.classList.add("popup-image");
        document.body.appendChild(imageElement);
        imageElement.style.display = "block";

        var rect = imageElement.getBoundingClientRect();
        var y1 = rect.top;
        console.log("Popup y1 = " + y1);

        var y2 = event.clientY;
        console.log("Popup y2 = " + y2);

        var y = -y1 + y2 -100;
        if (y < -y1){
            y = -y1;
        }
        console.log("Popup y = " + y);

        imageElement.style.top = y;
    }

    // ぼかしなし画像表示
    function showImage2(element,event) {
        event.preventDefault();

        var imageSrc = element.getAttribute("href");
        var imageElement = document.createElement("img");
        imageElement.src = imageSrc;
        imageElement.classList.add("popup-image2");
        document.body.appendChild(imageElement);
        imageElement.style.display = "block";

        var rect = imageElement.getBoundingClientRect();
        var y1 = rect.top;
        console.log("Popup y1 = " + y1);

        var y2 = event.clientY;
        console.log("Popup y2 = " + y2);

        var y = -y1 + y2 -100;
        if (y < -y1){
            y = -y1;
        }
        console.log("Popup y = " + y);

        imageElement.style.top = y;
    }
    
    // 画像消去
    function hideImage() {
        var popupImages = document.getElementsByClassName("popup-image");
        while (popupImages.length > 0) {
            var image = popupImages[0];
            image.parentNode.removeChild(image);
        }

        var popupImages2 = document.getElementsByClassName("popup-image2");
        while (popupImages2.length > 0) {
            var image2 = popupImages2[0];
            image2.parentNode.removeChild(image2);
        }
    }
</script>
  
</BODY></html>

[C++] 338 BBS閲覧アプリの製作 その24 GUIに変数が表示されない場合 / DL済レス数表示および自動スクロール JavaScript

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

これまではnew/deleteやmalloc/freeで変数をメモリに格納して解放するまで保持できるようにしていましたが、パブリック変数にするだけで同様の効果が得られることに今さら気が付きました。

変数名が分かってしまうとデータ改変の可能性が高まりますが、自分用のアプリだったら問題ないでしょう。まあ、スコープをむやみに広げるのは良くないですが。

リロード時刻をGUIに表示できず、少々手間取っているうちに他ソースファイルでの使用不使用に係わらずパブリック変数にしてしまえばいいという手抜きに気付いたのでした。

追加機能ですが、DL済レス数を表示できるようにしました。次にDATファイルをダウンロードして表示する際は前回ダウンロードした最後のレスへ自動スクロールします。

<script>
var element = document.getElementById(305);
var rect = element.getBoundingClientRect();
var y = rect.top;
console.log("toOldRes y = " + y);
scrollTo(0, y);
</script>

[C++] 337 BBS閲覧アプリの製作 その23 ポップアップ位置の改善 JavaScript

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

レスアンカーのポップアップの位置をFl_WebViewの先頭やや下に固定していましたが、視線の動きを減らすため、ホバーした位置の近くに変更しました。

正確にはホバー位置を基準に100px上方へずらしています。上にはみ出さないよう調整しています。

ところでアプリのサイズは今のところ2.7MBになります。curlやxml2ライブラリは含みません。かなり軽量でかつ高速です。

単機能Electronアプリ(JavaScript)210MBやJavaアプリを除くと、これまで作ってきたデスクトップアプリでは最大サイズになりました。ちなみにSwiftで作ったメモアプリiOS版、ChatGPTアプリiOS版はそれぞれ4.2MB、2.7MBです。

function showPopup(id, event) {
    var element = document.getElementById(id);
    element.style.visibility = (element.style.visibility == 'visible')? "hidden": "visible";

    var rect = element.getBoundingClientRect();
    var y1 = rect.top;
    console.log("Popup y1 = " + y1);

    var y2 = event.clientY;
    console.log("Popup y2 = " + y2);

    var y = -y1 + y2 -100;
    if (y < -y1){
        y = -y1;
    }
    console.log("Popup y = " + y);

    element.style.top = y;
}
<p id="251" class="tips"><font size="2" color="#008080">251 : 投稿者名
<br></font><font size="2" color="#000080">2023/07/23(日) 19:36:50.61 ID:xxx<br></font>
<font size="2" color="#000B00"> <span onmouseover = "showPopup(230,event);" onmouseout = "showPopup(230,event);" >
<a href="../xxx/230" rel="noopener noreferrer" target="_blank">>>230</a></span>
<br> 本文 </font></p>

[C++] 336 BBS閲覧アプリの製作 その22 Fl_WebViewにコンソール代替機能実装 JavaScript

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

Fl_WebViewはGoogle ChromeのようなWebブラウザに近い機能を持っていますが、さすがに検証機能はありません。

そこでコンソールのような機能を持たせるため、テスト用のポップアップ要素をあらかじめ仕込んでおき、必要時にテスト関数でポップアップに結果を出力させるようにしました。

下図の例では、GUI右下のスクロールボタンをホバーした時にテスト関数が動作し、その結果を右上のポップアップに出力しています。

この機能により、Fl_WebViewは最新のJavaScriptであるES2022に対応していることが分かりました。

普段はChromeの検証機能を使えばいいと思いますが、Fl_WebView固有の性質を調べる場合は今回の機能を使用します。

function test(){
    const msg = "This browser supports ES2022.";
    const msg2 = "This browser don't supports ES2022.";

    var element = document.getElementById("test");
    element.style.visibility = (element.style.visibility == 'visible')? "hidden": "visible";
    var rect = element.getBoundingClientRect();
    var y = rect.top;
    console.log("Popup y = " + y);
    element.style.top = -y + 20;

    try {
        eval('class Foo { #bar = 42; }');
        element.textContent = msg;
    } catch (error) {
        element.textContent = msg2;
    }
}