Ahkab Illustrator Text — 네이티브 일러스트레이터 텍스트 (문단 하나 = 텍스트 상자 하나)
By Ahmad Abdullahi Kabeer · Graphcent
이유
Illustrator는 가져온 SVG 및 PDF 텍스트를 항상 한 줄씩 하나의 텍스트 객체로 분할하여 그룹화합니다. 어떤 SVG 마크업도 이를 막을 수 없습니다. 따라서: 아트워크용 SVG 페이지를 유지하고, 사용자가 한 번 실행하는 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 렌더링용). - 레코드를 JS 객체 리터럴로 포함하여 .jsx를 생성합니다 (
json.dumps(..., ensure_ascii=True)— ExtendScript는 ES3이므로 JSON.parse가 필요 없음). - 생성된 .jsx를 Node에서 Illustrator DOM 모의(mock)에 대해 드라이런합니다 (
app,documents.add,open,textFrames.areaText/pointText,pathItems.rectangle,artboards,textFonts.getByName,enums). 확인: area/point 프레임 수, 모든 프레임에 내용 + 글꼴 + 이름이 있는지, rect 좌표가 숫자인지, 아트보드 = 페이지. - 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마다: 아트보드 rect [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);
// 리치 스팬: 첫 번째 이후의 실행(run) 중 무게/색상이 다른 경우, 문자 [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 내부에서 실행되지 않았음.