Ahkab Illustrator Text — 原生 Illustrator 文字(一個段落 = 一個文字框)
作者:Ahmad Abdullahi Kabeer · Graphcent
為何需要
Illustrator 總是將導入的 SVG 和 PDF 文字拆分為每行一個文字物件,並群組在一起。沒有任何 SVG 標記可以防止這種情況。因此:保留 SVG 頁面作為圖稿,並透過一個 Illustrator 使用者執行一次的 ExtendScript (.jsx) 構建腳本(檔案 > 指令碼 > 其他指令碼…)在 Illustrator 內部原生重建所有文字。該腳本會儲存一個真正的 .ai 檔案。
工作流程
- 在生成頁面時記錄文字。 每當頁面生成器產生
<text>時,也為每個頁面儲存一條記錄:{id, kind: 'area'|'point', layer: '05', x, y(第一基線,SVG y 向下,單位 pt), w(文字框寬度), lead(行距), n(換行行數), size(字級), anchor: 'start'|'middle'|'end', ls(字距,單位 pt), runs: [[text(文字), weight(字重), '#RRGGBB'], …], name(名稱)}- 換行段落(以及多行標題) ->
area。單行標籤、數字、引言 ->point。 - 使用與繪製 SVG 時完全相同的 x/y/寬度/行距,以便位置與 PDF 相符。
- 賦予有意義的名稱(例如
Name — slot 01、Biography — slot 04)。
- 換行段落(以及多行標題) ->
- SVG 頁面需求: 檔案
pages/NN_Name.svg(NN = 頁碼);頂層群組id="L01_Backgrounds"、L02_…等,並帶有data-name= 可讀的圖層名稱。在 SVG 中也保留文字(做為備用與 PDF 渲染)。 - 生成 .jsx 透過將記錄嵌入為一個 JS 物件字面值(
json.dumps(..., ensure_ascii=True)— ExtendScript 是 ES3,無需JSON.parse)。 - 乾運行 在 Node 中對 Illustrator DOM 的模擬(app、documents.add、open、textFrames.areaText/pointText、pathItems.rectangle、artboards、textFonts.getByName、enums)執行生成的 .jsx。檢查:區域/點文字框的數量、每個文字框都有內容 + 字型 + 名稱、矩形座標是數字、畫板 = 頁數。
- 在 README 中說明 可編輯的 .ai 來自執行該腳本;直接開啟 SVG/PDF 仍會得到拆分的文字行。清楚說明該腳本已在模擬環境中測試,而非在 Illustrator 本身中。
核心腳本邏輯(ExtendScript)
#target illustrator
var PAGE_TEXT = /* {"1":[records…], "2":[…]} */;
app.userInteractionLevel = UserInteractionLevel.DONTDISPLAYALERTS;
app.coordinateSystem = CoordinateSystem.DOCUMENTCOORDINATESYSTEM; // y 向上增加
// 1) doc = app.documents.add(DocumentColorSpace.RGB, W, H); 建立命名的圖層(doc.layers.add() 會新增在最上層)。
// 2) 每頁 p:畫板矩形 [left, top, left+W, top-H],其中 left = col*(W+GAP),top = -row*(H+GAP)。
// src = app.open(svgFile); 刪除導入的文字:
// for (var i = src.textFrames.length-1; i >= 0; i--) src.textFrames[i].remove();
// 尋找名稱符合 /^L?(0[1-9])/ 的群組/子圖層,並複製到匹配的圖層中:
// item.duplicate(targetLayer, ElementPlacement.PLACEATBEGINNING).translate(left - sr[0], top - sr[1]);
// (遍歷 container.pageItems,過濾 typename === 'GroupItem';不要使用 === 比較 DOM 物件)
// src.close(SaveOptions.DONOTSAVECHANGES); 然後 buildText(PAGE_TEXT[pageNo], left, top)。
var FONT_CANDIDATES = { 300:["Kreadon-L","Outfit-Light"], 400:["Kreadon-R","Kreadon-M","Outfit-Regular"],
500:["Kreadon-M","Outfit-Medium"], 600:["Kreadon-Demi","Kreadon-D","Outfit-SemiBold"],
700:["Kreadon-B","Outfit-Bold"], 800:["Kreadon-EB","Outfit-ExtraBold"] }; // 優先使用專案的 PostScript 名稱
function fontFor(w){ var n=FONT_CANDIDATES[w]||FONT_CANDIDATES[400]; for(var i=0;i<n.length;i++){ try{ return app.textFonts.getByName(n[i]); }catch(e){} } return null; }
function rgb(h){ var c=new RGBColor(); h=h.replace('#',''); c.red=parseInt(h.substr(0,2),16); c.green=parseInt(h.substr(2,2),16); c.blue=parseInt(h.substr(4,2),16); return c; }
function styleRange(r,w,size,col,lsPt,lead){ var a=r.characterAttributes, f=fontFor(w); if(f) a.textFont=f; a.size=size;
if(lead){ a.autoLeading=false; a.leading=lead; } a.fillColor=rgb(col); a.tracking=Math.round((lsPt||0)/size*1000); }
function buildText(recs, left, top){
for (var i=0;i<recs.length;i++){ var t=recs[i], layer=layerByKey[t.layer]||layerByKey['06'], s='', tf;
for (var r=0;r<t.runs.length;r++) s+=t.runs[r][0];
if (t.kind==='area'){ var lead=t.lead||t.size*1.4;
var rect=layer.pathItems.rectangle(top-(t.y-lead), left+t.x-1, t.w+2, (t.n+1)*lead); // 為字型替換多預留 +1 行
tf=layer.textFrames.areaText(rect); tf.contents=s; styleRange(tf.textRange,t.runs[0][1],t.size,t.runs[0][2],t.ls,lead);
try{ tf.firstBaseline=FirstBaselineType.LEADING; }catch(e){ tf.translate(0,-(lead-t.size*0.95)); }
} else { tf=layer.textFrames.pointText([left+t.x, top-t.y]); tf.contents=s; styleRange(tf.textRange,t.runs[0][1],t.size,t.runs[0][2],t.ls,null); }
tf.textRange.paragraphAttributes.justification = t.anchor==='middle'?Justification.CENTER:(t.anchor==='end'?Justification.RIGHT:Justification.LEFT);
// 豐富區段:對於第一個之後、字重/顏色不同的區段,設定字元 [pos, pos+len) 的樣式:
// try { var rr=tf.textRange.characters[pos]; rr.length=len; styleRange(rr,…); } catch(e){ 逐字元迴圈 }
tf.name = t.name || t.id; } }
// 3) IllustratorSaveOptions {pdfCompatible:true, compressed:true, embedLinkedFiles:true}; doc.saveAs(File(root+'/<Name>.ai'));
// 顯示最終的提示對話框,附帶任何備註(例如缺少的字型)。將所有內容包在 try/catch 中,並恢復 userInteractionLevel。
交付前的檢查
- 模擬乾運行通過;段落數量與產生器中換行文字區塊的數量相符。
- 區域文字框使用原始寬度(+2pt),以便換行與 PDF 一致;如果使用較寬的替代字型,文字會在框內重新排列。
- README 說明:執行腳本以獲得可編輯的 .ai;如果要更換字型,使用尋找/取代字型;該腳本並非由 Claude 在 Illustrator 內部執行。