DTPab

印刷やデザイン、アドビ製アプリやスクリプトなど、雑多な技術ブログ

ExtendScriptでXMPメタデータを取り扱う

f:id:uske_S:20210304101250p:plain
概念図

InDesignのファイルに配置したIllustratorのファイルに配置された画像ファイル(以下、便宜的に孫リンクと表記)を探し出したいなーと思ってXMPメタデータをいじることにしたのですが、これが思いの外わかりづらくて苦労しました。今後のために記録しておこう、というエントリーです。

ここまでまとめておけばこの先またXMPメタデータのマニピュレーションで迷っても安心だなと書き上げてから思いましたw
というわけでXMPメタデータを操作したい方はぜひ参考にしてください。
ちなみに、記事中のスクリプトについてはすべてInDesignからの操作です。他のAdobeアプリケーションでも同様に動くと思いますが未検証です。

XMPメタデータの概要

XMPとは

Adobeの公式資料によると下記のように示されています。

Extensible Metadata Platform(XMP)は、すべてのメタデータ管理に対して Experience Manager Assets で使用されるオープンな標準です。XMP は、すべてのファイル形式に埋め込むことができる、ユニバーサルメタデータエンコーディングを提供します。アドビやその他の企業は、リッチコンテンツモデルを提供する XMP 標準をサポートしています。XMP 標準および Experience Manager Assets のユーザーは、基盤となる強力なプラットフォームを持っています。詳しくは、XMP*1を参照してください。

日本語がわかりにくいですが、名前に Platform とあるようにメタデータを管理するための規格のようなものです。
なお、記述される構文はXMLです。XMP自体はAdobeが開発元で、技術的にはW3CのRDF(Resource Description Framework)*2が使われており、オープンライセンスとして提供されているようです*3

メタデータとは

平たくいうと、データに関する様々な情報です。メタデータについては細かく説明する必要もなかろうかと思います。

XMPメタデータに記述されている情報群

下記はaiファイルのXMPメタデータに記述されている内容の例です*4

  • 作成・保存日時
  • 作業アプリケーション
  • 保存履歴
  • リンクファイル
  • プロファイル(印刷用かweb用か)
  • ページサイズ
  • 単位設定
  • 使用フォント
  • 使用版(プレート)
  • スウォッチ

今回はこの中からリンクファイルに関する情報を取り出します。

リンクファイル情報の記述をどう取り出すか

アプローチとしては3つ考えました(いずれもInDesignからの操作)。

  1. テキストデータとしてaiファイルを開いて総当り的に検索する
  2. Link.linkXmpからメタデータにアクセスする
  3. XMPのライブラリを使ってXMPメタデータにアクセス

1については無駄が多いので考慮しませんでした。2と3をチャレンジして、結果的に3を採用した形です。

Link.linkXmpからのアクセス

LinkオブジェクトのプロパティlinkXMPを参照し、linkMetadata オブジェクトを漁りに行く方法です。
var a = app.selection[0].itemLink.linkXmp;などとしてブレークポイントを設定してスクリプトを走らせると、下記のようなプロパティが見つかります。

プロパティ名 概要
creationDate ファイル作成日時
creator 編集アプリケーション名
documentTitle ファイル名
format ファイルフォーマット
modificationDate 編集日時

いい方を変えると、これら以外は自前で情報を探し出すことになります。それを行うのがgetProperty()メソッドです。アクセスしたい要素名を第2引数に、その要素の名前空間*5を第1引数に渡すことで、特定の要素にアクセスすることができるものです。後述しますが、linkXmpにはgetArrayItem()メソッドがないみたいなので、今回は3の方法(FileオブジェクトからXMPメタデータにアクセス)を採用することにしました。

XMPのライブラリを使ってXMPメタデータにアクセス

手順としては下記のようになります(具体的なコードは後述)。

  1. XMPのライブラリを読み込む
  2. 対象となるFileオブジェクトのXMPのデータをシリアライズする
  3. 2がXMPメタデータオブジェクトとしてアクセスできるようになる

このやり方が全てのファイルに対してうまくいくかはわかりませんが、今回の孫リンクを探し出すという用途においてaiファイルに対しては有用でした。
次の関門は、アクセスできるようになったものの任意の要素に対してどのようにアプローチするのか、というXMPのお作法が分からないことでした…。

XMPメタデータの任意の要素にアクセス

そのお作法を教えてくれたのがTen_A先生の下記ブログ記事2件です。ぶっちゃけここさえ読めば僕の書いたこのエントリーの9割が不要です。たぶん。
基本的には前述したとおりgetProperty()メソッドで任意の要素にアクセスします*6。が、XMPの仕様を少し理解しておかないと単純にはアクセスできない要素もあります。

ten-artai.com

こちらはXMPメタデータをマニピュレーションするための基本的な解説です。まずはこれを読んでXMPメタデータの扱い方を理解しましょう。XMPメタデータを扱うならこの記事は必読です。なんならPDFにするなどアーカイブとして残しておくべき聖典です。

ten-artai.com

続いてこちらはその応用例です。今回孫リンクを探し出すに当たってクリティカルに欲しい情報がここから得られました。

aiファイルのXMPメタデータをBridgeから確認する

さて、ここからはもうちょっと具体的に突っ込んでいきます。
まずは読み込むためのaiファイルを適当に用意します(孫リンクを調べたいので画像を配置しておきます)。

f:id:uske_S:20210327152748p:plain
用意したaiデータ

このaiファイルのXMPメタデータを確認してみましょう。
Bridgeを起動し、ファイルを右クリックしてコンテクストメニューを開いてください。

f:id:uske_S:20210327153730p:plain
ファイル情報をクリック

「ファイル情報」をクリックするとファイルのメタデータを俯瞰でき、左側カラムのいちばん下に「Rawデータ」があります。

f:id:uske_S:20210327153749p:plain
Rawデータをクリック

これがaiデータのXMPメタデータの全文になります。

ネームスペース

ネームスペースが記述されているのがXMPメタデータの冒頭部分です。ネームスペース(名前空間)が一体何なのかは僕自身もきちんと説明できないので、ほかのサイトや文献等をご参照ください。以下ではあくまで「こうやれば目的のプロパティを参照できるよ」という手法についてのみ解説します。

</div>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 6.0-c006 79.164648, 2021/01/12-15:52:29        ">
   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
      <rdf:Description rdf:about=""
            xmlns:dc="http://purl.org/dc/elements/1.1/"
            xmlns:xmp="http://ns.adobe.com/xap/1.0/"
            xmlns:xmpGImg="http://ns.adobe.com/xap/1.0/g/img/"
            xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
            xmlns:stRef="http://ns.adobe.com/xap/1.0/sType/ResourceRef#"
            xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#"
            xmlns:stMfs="http://ns.adobe.com/xap/1.0/sType/ManifestItem#"
            xmlns:illustrator="http://ns.adobe.com/illustrator/1.0/"
            xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/"
            xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#"
            xmlns:stFnt="http://ns.adobe.com/xap/1.0/sType/Font#"
            xmlns:xmpG="http://ns.adobe.com/xap/1.0/g/"
            xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
このxmlnsで始まるところが丸っとネームスペースになります。使い方は後述しますので、ここに記述されている、ということを覚えておいてください。

aiファイルにリンクされた画像のパス

目的のリンクファイルについては<xmpMM:Manifest>というところに記述されます。そこを抜粋したのが下記です。

</div>
<xmpMM:Manifest>
    <rdf:Seq>
        <rdf:li rdf:parseType="Resource">
            <stMfs:linkForm>EmbedByReference</stMfs:linkForm>
            <stMfs:reference rdf:parseType="Resource">
                <stRef:filePath>hoge(ファイルパス)</stRef:filePath>

この「hoge(ファイルパス)」の部分が取得できれば今回の目的は達成です。

コードに落とし込む

以上を踏まえて、XMPメタデータの任意の要素にアクセスする方法を実際のExtendScriptでどのようにするのか書いていきます。

ライブラリの読み込み

まずはXMPメタデータを扱うためのライブラリを読み込みます。
先のTen_A先生のひとつめの記事から、その部分を拝借して書き換えましょう。

var targetFile = File('対象となるaiファイルのパス')
if (!xmpLib) { var xmpLib = new ExternalObject('lib:AdobeXMPScript'); }
var xmpFile = new XMPFile(targetFile, XMPConst.FILE_ILLUSTRATOR, XMPConst.OPEN_FOR_READ);

Ten_A先生によると、

このライブラリに限らず、ExternalObjectの類いは複数のインスタンスを読み込むとアプリケーションの安定性に影響を及ぼします。ですから、インスタンスが存在するかどうかをまずチェックしてからライブラリを読み込みます。

とのことですので、すでに読み込んでいるならばそれ以上読み込まない処理を施した上で、XMPFileオブジェクトのインスタンスを生成します。引数は順に読み込むファイル、ファイルフォーマット(Enum定数)、ファイルを開く際のオプション(Enum定数)です*7

主なファイルフォーマットについては下記*8の通りです。

フォーマット XMPConst.Enum値
未定 XMPConst.FILE_UNKNOWN
PDF XMPConst.FILE_PDF
Photoshop XMPConst.FILE_PHOTOSHOP
Illustrator XMPConst.FILE_ILLUSTRATOR
InDesign XMPConst.FILE_INDESIGN

今回はaiファイルを対象とするため、XMPConst.FILE_ILLUSTRATORとしました*9

ファイルを開く際の主なオプションは下記の通り。

読み込みオプション XMPConst.Enum値
読み込みのみ XMPConst.OPEN_FOR_READ
読み書き可能 XMPConst.OPEN_FOR_UPDATE

XMPパケットのシリアライズ

var xmpPackets = xmpFile.getXMP();
var xmp = new XMPMeta(xmpPackets.serialize());
ここまではほぼ定型句です。ここまでやれば、データがシリアライズされて読み込むことができるようになります。

プロパティへのアクセス

データをシリアライズできたら、XMPMetaObj.getProperty()メソッドを使って任意のプロパティにアクセスします。このメソッドの引数は下記*10のとおりです。

XMPMetaObj.getProperty(schemaNS, propName[, valueType])

パラメーター 意味
schemaNS ネームスペースURI文字列、要するにネームスペースのこと。一部はEnum値として用意*11されています。
propName プロパティ名文字列。階層表現が可能です。
valueType 省略可、文字列型。プロパティのデータ型を指定。例:XMPConst.STRINGXMPConst.INTEGERなど。

得られるオブジェクトはいくつかプロパティを持っています*12。そのうち、valueプロパティに目的の文字列が入っているのでそれにアクセスします。

先ほどシリアライズしたxmp変数にこのメソッドを用いて、下記のように処理できます。

xmp.getProperty(XMPConst.NS_XMP_MM, "Manifest[1]/stMfs:reference/stRef:filePath").value

こうすることで目的の文字列(リンクファイルのファイルパス)が取得できます。このメソッドの2つの引数について、それぞれ説明します。

getProperty()メソッドの第一引数:ネームスペースの指定

ネームスペースの指定にはやり方が2つあります。ひとつは、前述したxmlnsと記述されているところの文字列(URI)をそのままコピペする方法。もうひとつは用意されたネームスペース文字列定数を用いる方法です。

リンクファイルのパスが記述されている部分のXMLについて下記に再掲します。

</div>
<xmpMM:Manifest>
    <rdf:Seq>
        <rdf:li rdf:parseType="Resource">
            <stMfs:linkForm>EmbedByReference</stMfs:linkForm>
            <stMfs:reference rdf:parseType="Resource">
                <stRef:filePath>hoge(ファイルパス)</stRef:filePath>

最初のタグ<xmpMM:Manifest>の部分が、どのネームスペースを指定すればいいかを教えてくれています。ネームスペースの一覧を下記に再掲します。

</div>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 6.0-c006 79.164648, 2021/01/12-15:52:29        ">
   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
      <rdf:Description rdf:about=""
            xmlns:dc="http://purl.org/dc/elements/1.1/"
            xmlns:xmp="http://ns.adobe.com/xap/1.0/"
            xmlns:xmpGImg="http://ns.adobe.com/xap/1.0/g/img/"
            xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
            xmlns:stRef="http://ns.adobe.com/xap/1.0/sType/ResourceRef#"
            xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#"
            xmlns:stMfs="http://ns.adobe.com/xap/1.0/sType/ManifestItem#"
            xmlns:illustrator="http://ns.adobe.com/illustrator/1.0/"
            xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/"
            xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#"
            xmlns:stFnt="http://ns.adobe.com/xap/1.0/sType/Font#"
            xmlns:xmpG="http://ns.adobe.com/xap/1.0/g/"
            xmlns:pdf="http://ns.adobe.com/pdf/1.3/">

これらのネームスペースの中から、xmpMMのネームスペースを参照するとxmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"になることがわかります。これはxmpMMのネームスペースURIは"http://ns.adobe.com/xap/1.0/mm/"ということです。これをgetPropery()メソッドの第一引数に直接記述してもOKです。

ただ今回は(せっかく定数として用意されているし)ふたつめのやり方で指定します。
用意されているネームスペース文字列定数*13の中から、xmpMMのネームスペースを探すとXMPConst.NS_XMP_MMとなることがわかります。これを第一引数に指定します。

getProperty()メソッドの第二引数:プロパティの指定

これがややこしかったw
今回の記事はここについて書きたいがためといっても過言ではないです。まず今回の正しい記述をおさらいします。

"Manifest[1]/stMfs:reference/stRef:filePath"

XMPMetaObj.getProperty()メソッドのところの引数の説明で階層表現が可能と書きましたが、この書き方ズバリです。要するにスラッシュで階層構造を表現できるのです。
改めて<xmpMM:Manifest>の中のタグを一つずつたどっていくと、

<xmpMM:Manifest>
<rdf:Seq>
<rdf:li rdf:parseType="Resource">
<stMfs:reference rdf:parseType="Resource">
<stRef:filePath>

となっています。
これらのうち、rdf:で始まるものはXMLのプロパティではなく、RDFの構造を示しています。Seqというのはシーケンス(Sequence)のことで、わかりやすくいうと配列のようなものですね。liも同様です。なのでここはプロパティの記述には不要です。したがって、必要なのはxmpMM:ManifeststMfs:referencestRef:filePathとなります。
最初のxmpMM:Manifestについては、ネームスペースでxmpMMを指定しているのでManifestから書き始めます。が、この中身がシーケンスになっているので配列のような表記にする必要があります。それが引数のManifest[1]になります。XML(というかRDF)は1から始まるインデックスですので、1つめの要素へのアクセスは[1]です。これを順にスラッシュでつなげてManifest[1]/stMfs:reference/stRef:filePathとすることで、階層構造を指定したプロパティ名になります。JSのようにプロパティのアクセスを.でたどっていかないことに注意してください。

実際にはシーケンスの内容をすべて調べたいので、シーケンス内の個数を把握した上でManifest[1]Manifest[2]…と必要なだけ処理する必要があります。シーケンスの中身の個数を調べるにはXMPMetaObj.countArrayItems()メソッドを使ってください(使い方はgetProperty()メソッドと同じなので割愛します。後掲のコード全体を見ていただければ分かるはずです)。

コード全体(サンプル)

以上をまとめると下記のようなコードになります。

/
 * 配置画像にリンクされた孫リンクのファイルパスを配列で返す
 
 * @param {File} target 配置リンクファイルのFileオブジェクト
 * @returns {string[]} ファイルパス文字列の配列
 /
function getLinkedImageFilePath(target) {
    // AdobeのXMPライブラリの読み込み
    if (!ExternalObject.AdobeXMPScript) {
        ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript');
    }
    var xmpFile = new XMPFile(target.fsName, XMPConst.UNKNOWN, XMPConst.OPEN_FOR_UPDATE);
    var xmpPackets = xmpFile.getXMP();
    var xmp = new XMPMeta(xmpPackets.serialize());
    // ここまで定型句
    // 以下、ネームスペースと要素を指定して getProperty(対応するネームスペース, 要素名) メソッドで値を得る
    var ns = XMPConst.NS_XMP_MM;
    var count = xmp.countArrayItems(ns, "Manifest"); // 要素が複数ある場合 countArrayItems() メソッドで要素数を数える(戻り値はNumber型)
    var result = [];
    // XMPは 1ベース のインデックスなのでfor文は1から回す
    for (var i = 1; i <= count; i++) {
        // 要素名 Manifest に複数要素が含まれているため Manifest[i] とし、その先の要素はXMPに記述されたタグ同士を / で連結して表記する
        result.push(xmp.getProperty(ns, "Manifest[" + i + "]/stMfs:reference/stRef:filePath").value); // value プロパティに値が入っている
    }
    return result;
}

関数の引数にはFileオブジェクトを渡してください。InDesignからであれば、File(Link.filePath)みたいな形で引数に渡します。

最後に

以上めちゃくちゃ長くなりましたが、InDesignのExtendScriptでXMPメタデータを参照する方法を細かく解説しました。しっかり調べながら書いたつもりですが、間違いや勘違いなどがあればぜひご指摘ください。

*1:リンク先(英語):
File management, metadata integration | Adobe Extensible Metadata Platform (XMP)

*2:RDFについては本家W3Cの翻訳版がこちら(非常に分かりやすいが超ボリューム):
RDF入門

*3:https://www.antenna.co.jp/xml/xmllist/XMP/AboutXMP.htm

*4:これらが全てのaiファイルに含まれているわけではなく、かつここにないものが含まれる場合もありますし、自分で新たに記述することもできます

*5:XMPメタデータ冒頭に記述されるネームスペースのこと。詳しくはXMP(XMLとRDF)の仕様になるのでこのエントリーでは割愛します

*6:簡易的にXMPメタデータを読み書きするツールを、やはりTen_A先生が公開してくださっています(僕はこれを常用しています):
XMPメタデータをExtendscriptから操作するライブラリXMPtool – Automation Skill

*7:XMPFileコンストラクタはAdobeの公式資料であるJavaScript Tools Guideに掲載されています(リンク先は非公式だけど探しやすいので重宝しています)

*8:File format numeric constants

*9:Ten_A先生の記事にあるように XMPConst.FILE_UNKNOWN のままでも動作します。ただXMPConstについての説明文を読むとこのように設定したほうが妥当だと感じたので、今回は XMPConst.FILE_ILLUSTRATOR を採用しました

*10:https://extendscript.docsforadobe.dev/scripting-xmp/xmpscript-object-reference.html#xmpmetaobj-getproperty

*11:https://extendscript.docsforadobe.dev/scripting-xmp/xmpscript-object-reference.html#schema-namespace-string-constants

*12:詳しくはこちらを参照:XMPProperty object properties

*13:一覧はこちら:Schema namespace string constants