複数のAIファイルの文字をアウトライン化する処理例です。入力と出力を別フォルダーに分け、元の名前に「_ol」を付けたAIを保存します。同名の出力はスキップし、ファイルごとの結果をログに残します。
2026年10月3日にコードを改修し、模擬オブジェクトによる試験と、Windows 11 Pro/Illustrator 30.8.2(64-bit)での限定条件の実機試験を行いました。確認した入力・結果と、未確認の条件は後述します。Macの実機試験は行っていません。コピーした少数のファイルで試し、出力を開いて確認してから用途に合わせて使います。
対象になる文字と処理を止める条件
doc.textFramesから取得した文字を一度配列へ控え、順番にcreateOutline()を呼びます。変換中にコレクションの件数が変わっても、次の文字を飛ばさないための手順です。取得した文字に編集不可・ロック・非表示の状態がある場合は、そのファイルの変換結果を保存しません。親グループとレイヤーも確認し、ロックや表示状態を勝手に変更しない構成にしています。
この確認はtextFramesとして参照できる範囲に限ります。シンボル、プラグインオブジェクト、配置したPDFやEPSの内部などにある文字をすべて検出・変換する保証はありません。フォント不足やオーバーセット、外観の変化も、このコードだけでは判定できません。出力を開いて確認してください。
アウトライン化が必要か確認する
印刷会社や納品先の指定を先に確認します。編集可能な文字を残す必要がある場合は、元のAIを別途保管します。PDFでのフォント埋め込みを指定されている場合など、すべての文字をアウトライン化する必要があるとは限りません。入稿条件の確認とリンク・フォントを収集するパッケージも参照してください。
実行の手順
- 作業に使うAIをコピーし、入力用フォルダーへ3件ほど入れる。サブフォルダーは対象外です。
- 入力とは別の出力用フォルダーを作る。ほかのアプリや同期処理から同じ出力先へ書き込まない。
- Illustratorで開いているドキュメントを保存して閉じる。入力AIを1件ずつ手動で開き、フォント・リンク・見た目を確認して閉じる。
- 掲載コードをUTF-8のテキストとして保存する。日本語が化ける場合はBOM付きUTF-8にする。拡張子が「.jsx.txt」になっていないか確認する。
- 「ファイル」→「スクリプト」→「その他のスクリプト」から実行する。外部JSXの警告は内容を確認し、この手順では無効化しません。
- 下のコードをoutlineAll.jsxとして保存する。
- コピーした入力フォルダーと別の出力フォルダーを選ぶ。入力の直下だけを処理します。
- 出力の「元の名前_ol.ai」とログを照合する。ロック・非表示などで失敗した入力は、手動で内容を確認して扱いを決める。
- 元のAIと出力を開き、文字の見た目、改行、位置、外観、レイヤー構成を比較する。
一括アウトラインのコード
// Illustrator実機未検証。コピーした少数のAIで試してください。
(function () {
var OUTPUT_SUFFIX = "_ol.ai";
var LOG_PREFIX = "outline_batch_";
var RUN_NOTE = "ロック・非表示の文字があるファイルは保存しない";
function clean(value) {
return String(value).replace(/[\t\r\n]+/g, " ");
}
function logRow(log, status, input, output, note) {
log.encoding = "UTF-8";
if (!log.open("a")) throw new Error("ログを開けません: " + log.error);
var written = false;
var closed = false;
try {
written = log.writeln([new Date().toString(), status, clean(input),
clean(output), clean(note)].join("\t"));
} finally {
closed = log.close();
}
if (!written || !closed) throw new Error("ログを保存できません: " + log.error);
}
function makeLog(dst) {
var stamp = new Date().getTime();
var number = 0;
var log;
do {
log = new File(dst.fsName + "/" + LOG_PREFIX + stamp + "_" + number + ".tsv");
number++;
} while (log.exists);
log.encoding = "UTF-8";
if (!log.open("w")) throw new Error("ログを作成できません: " + log.error);
var written = false;
var closed = false;
try {
written = log.writeln("time\tstatus\tinput\toutput\tnote");
} finally {
closed = log.close();
}
if (!written || !closed) throw new Error("ログを保存できません: " + log.error);
return log;
}
function processDocument(doc) {
var frames = [];
var i;
for (i = 0; i < doc.textFrames.length; i++) {
var frame = doc.textFrames[i];
if (!frame.editable) throw new Error("編集できない文字があります");
var parent = frame;
while (parent && parent.typename !== "Document") {
if (parent.typename === "Layer") {
if (parent.locked || !parent.visible) throw new Error("ロック・非表示のレイヤーに文字があります");
} else if (parent.locked || parent.hidden) {
throw new Error("ロック・非表示の文字または親オブジェクトがあります");
}
parent = parent.parent;
}
frames.push(frame);
}
for (i = 0; i < frames.length; i++) frames[i].createOutline();
if (doc.textFrames.length !== 0) throw new Error("参照範囲内に未処理の文字が残っています");
return "textFrames=" + frames.length;
}
function saveDocument(doc, output) {
doc.saveAs(output, new IllustratorSaveOptions());
}
function main() {
if (app.documents.length !== 0) {
alert("開いているドキュメントを保存して閉じてから実行してください。");
return;
}
var src = Folder.selectDialog("コピーした入力AIのフォルダーを選択");
if (!src) return;
var dst = Folder.selectDialog("入力とは別の出力フォルダーを選択");
if (!dst) return;
if (src.fsName.toLowerCase() === dst.fsName.toLowerCase()) {
alert("入力と出力は別のフォルダーにしてください。");
return;
}
var files = src.getFiles(function (entry) {
return entry instanceof File && /\.ai$/i.test(entry.name);
});
if (files.length === 0) {
alert("入力フォルダー直下にAIファイルがありません。");
return;
}
var log = makeLog(dst);
logRow(log, "START", src.fsName, dst.fsName, RUN_NOTE);
var done = 0;
var failed = 0;
var skipped = 0;
var processed = 0;
var halted = false;
for (var fileIndex = 0; fileIndex < files.length; fileIndex++) {
var input = files[fileIndex];
var name = File.decode(input.name).replace(/\.ai$/i, "") + OUTPUT_SUFFIX;
var output = new File(dst.fsName + "/" + name);
var doc = null;
var status = "OK";
var note = "";
if (output.exists) {
status = "SKIP";
note = "同名の出力があるため処理しません";
} else {
try {
doc = app.open(input);
note = processDocument(doc);
// 開いている間に出力が作られた場合も保存を避ける。
output = new File(output.fsName);
if (output.exists) {
status = "SKIP";
note = "保存直前に同名の出力を検出";
} else {
saveDocument(doc, output);
if (!new File(output.fsName).exists) {
throw new Error("保存後に出力ファイルを確認できません");
}
}
} catch (error) {
status = "ERROR";
note = String(error);
} finally {
if (doc !== null) {
try {
doc.close(SaveOptions.DONOTSAVECHANGES);
} catch (closeError) {
status = "ERROR";
note += " / 終了失敗: " + closeError;
halted = true;
}
}
}
}
processed++;
if (status === "OK") done++;
else if (status === "SKIP") skipped++;
else failed++;
logRow(log, status, input.fsName, output.fsName, note);
// 開く処理の途中でドキュメントが残った場合も継続しない。
if (app.documents.length !== 0) halted = true;
if (halted) break;
}
var remaining = files.length - processed;
var summary = "保存 " + done + " / 失敗 " + failed + " / スキップ " + skipped +
" / 未処理 " + remaining;
logRow(log, halted ? "HALTED" : "END", "", "", summary);
alert(summary + "\nログ: " + log.fsName +
"\nERRORの出力は不完全な可能性があります。開いて確認してください。");
}
try {
main();
} catch (fatalError) {
alert("処理を停止しました: " + fatalError +
"\n出力とログ、開いているドキュメントを確認してください。");
}
}());
ログと失敗したファイルの確認
出力フォルダーにTSV形式のログを作ります。各行は日時、状態、入力パス、出力パス、補足の順です。テキストエディターで開き、入力件数と「保存・失敗・スキップ・未処理」の合計を照合します。入力フォルダーに残したファイルは変更していないか、更新日時や内容でも確認します。
| 状態 | 意味と次の確認 |
|---|---|
| OK | 保存処理が戻り、出力ファイルの存在を確認した状態。見た目や印刷条件への適合は判定していません。 |
| SKIP | 同名の出力があったため保存を見送った状態。既存ファイルが今回の入力と一致するとは限りません。 |
| ERROR | 開く・処理する・保存する・閉じる、のいずれかで例外が起きた状態。出力があっても不完全な可能性があるため開いて確認します。 |
| HALTED/未処理 | ドキュメントを閉じられないなどの理由で後続を停止した状態。開いているファイルとログを確認し、原因を解消してから再開します。 |
通常のファイル別エラーはログに記録して次へ進みます。ただし、ドキュメントが残った場合やログを保存できない場合は停止します。強制終了・アプリのクラッシュでは最後の行が残らない場合もあります。ERRORの出力を自動削除する処理は入れていません。再実行時も既存出力はスキップされるので、不完全な出力を別の場所へ退避してから必要な入力だけを再処理します。
同名チェックは保存前に行いますが、別の処理が同時に出力を作る状況まで排他的に防ぐ仕組みではありません。同じ出力先を使うスクリプトを並行して実行しないでください。入力・出力を分ける判定はパス文字列によるため、同じフォルダーを指す別名・エイリアスも選ばないでください。
実機で確認するケース
- テキストの数が異なる3ファイルを処理し、全ファイルの出力と文字の結果を照合する。
- 文字のないAI、グループ内文字、サブレイヤー内文字、エリア内文字、パス上文字を別々に確認する。
- ロック・非表示の文字があるAIでERRORになり、そのファイルの変換途中のデータを保存しないことを確認する。
- 日本語・複数ドット・大文字拡張子の名前と同名「_ol.ai」を使い、既存出力の内容と更新日時を照合する。
旧コードからの変更と関連する処理
2026年10月3日に、入力と同じ場所へ保存する旧コードから、出力先を選ぶ方式へ改修しました。グローバル変数の競合と不要なロック解除をなくし、失敗記録と同名回避を加えています。過去のGitHub版は本文と一致するとは限らないため、今回のコードと混ぜずに確認してください。
処理方式を選ぶ場合は一括処理の使い分け、PDFへ変換する場合はPDF一括変換、JSXの保存・実行はスクリプトの始め方へ進んでください。
参照:Adobeのスクリプトの実行手順とJavaScript Tools Guide CC。文字のAPIはAdobe資料をもとに維持されているTextFrameItemの参照ページに説明があります。
Windows実機で確認したアウトラインとスキップ
2026年10月3日14:52〜14:58(日本時間)、Windows 11 Pro/Illustrator 30.8.2で、自作入力4件を専用フォルダーへ出力しました。ハーネスで入力・出力フォルダーだけを固定し、掲載コードの文字参照・変換・保存・同名判定は維持しています。
| 入力 | 初回結果 | 出力を再度開いた結果 |
|---|---|---|
| 名札.v2.ai | OK/文字2件 | 名札.v2_ol.ai:textFrames=0、1アートボード |
| sample-two.AI | OK/文字2件 | sample-two_ol.ai:textFrames=0、2アートボード |
| no-text.ai | OK/文字なし | no-text_ol.ai:textFrames=0、1アートボード |
| locked.ai | ERROR/編集できない文字 | locked_ol.aiは作成しない |
文字のある2件にはサブレイヤー内の文字を含めました。ロックを含む入力のエラー後も、残る3件は保存できました。再実行は既存の3出力をSKIPと記録し、locked.aiは再びERRORになりました。ロック解除や既存出力の作り直しは行っていません。
今回の初回試験前には入力のハッシュを採っていないため、入力ファイルのバイト列が不変だったという実測はありません。また、文字の外観・改行・パス上文字・エリア内文字・非表示文字・シンボル内文字、Macでの動作は実機未確認です。textFrames=0だけで納品品質を保証しません。

コメント