Ahkab Illustrator Text — 原生 Illustrator 文本(一个段落 = 一个文本框)
作者:Ahmad Abdullahi Kabeer · Graphcent
为什么
Illustrator 始终将导入的 SVG 和 PDF 文本分割为每个一行一个文本对象,并分组在一起。没有任何 SVG 标记可以阻止这种情况。因此:保留 SVG 页面用于图稿,并使用一个 ExtendScript (.jsx) 构建脚本在 Illustrator 内部原生重建所有文本,用户只需运行一次(文件 > 脚本 > 其他脚本...)。该脚本会保存一个真正的 .ai 文件。
工作流程
- 在生成页面时记录文本。 无论页面生成器在何处输出
<text>,同时按页存储一条记录:{id, kind: 'area'|'point', layer: '05', x, y(第一条基线,SVG 向下为正,单位 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.js 中针对 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 内部运行。