실제 제작 소스에서 찾은 질문을 독립 합성 코드로 다시 만들었다. 전체 구현과 실행 예제, 테스트를 함께 공개하며 원래 업체의 프로그램과 자료는 연결하지 않는다.
단어와 글줄의 경계를 명시한다
wrap은 단어 폭을 재는 함수를 받는다. 다음 단어가 폭을 넘으면 새 줄로 보내지만 단어 자체가 너무 길면 오류다. 이미 앞줄이 채워진 뒤 긴 단어가 나와도 같은 기준으로 거절한다. 최대 줄 수를 넘을 때 잘라 버리지 않고 레이아웃을 바꾸도록 실패한다.
이 계약은 실제 교육 자료 생성기의 단어 보존 질문에서 출발했다. 공유 PDF는 새 영어 문장과 독립 도형만 사용한다. 원래 활동지와 고객 폰트, 수업 사진은 옮기지 않았다. 언어마다 다른 분절 규칙은 별도로 설계해야 한다.
글줄과 절취선을 같은 포인트에서 확인한다
baselines는 위쪽 시작점과 행간으로 각 기준선을 만들고 마지막 줄이 예약한 하단을 넘으면 거절한다. dashes는 A4 포인트 좌표에서 일정한 길이와 간격의 선분을 만들며 마지막 선은 끝점에서 자른다. 페이지 밖의 선과 0 간격 진행을 막는다.
예제는 본문 프레임, 절취선과 하단 사용 안내를 서로 떨어뜨린다. 화면에서 점선이 예뻐 보이는 것과 종이에서 자르기 편한 것은 다른 검수다. 프린터의 배율과 인쇄 가능한 영역을 읽지 않으므로 실제 출력 설정은 별도로 확인해야 한다.
전체 구현과 PDF 생성 경로를 공개한다
핵심 테스트는 표준 라이브러리만 쓴다. make_pdf를 호출해 PDF를 생성하려면 ReportLab 4.4.9가 필요하며 출력 경로를 직접 전달한다. 실행 예제는 파일을 만들지 않고 기하 기록만 반환한다. ZIP 안의 sample.pdf는 실제로 생성해 검사한 고정 결과다.
한 페이지의 텍스트 추출과 사용 안내 일치를 확인하고 렌더링한 이미지를 눈으로 검토했다. 글자·도형·절취선과 페이지 번호가 겹치지 않았다. 물리적인 출력과 모든 글꼴 대체 환경을 검수했다는 뜻은 아니다. 실제 프로그램 원본은 실행하지 않았다.
코드와 구현 살펴보기
독립 합성 전체 구현
"""Synthetic word layout and explicit PDF creation. Units are points."""
import math
A4=(595.2756,841.8898)
NOTICE='Synthetic original shapes - modify and reuse permitted - v1'
def positive(n):
if isinstance(n,bool) or not isinstance(n,(int,float)) or not math.isfinite(n) or n<=0:raise ValueError('finite positive geometry')
return n
def nonnegative(n):
if isinstance(n,bool) or not isinstance(n,(int,float)) or not math.isfinite(n) or n<0:raise ValueError('finite nonnegative coordinate')
return n
def wrap(text,width,measure,max_lines=100):
positive(width)
if not isinstance(text,str) or not text.strip() or not isinstance(max_lines,int) or isinstance(max_lines,bool) or max_lines<1:raise ValueError('text and bounded lines')
words=text.split();lines=[];line=''
for word in words:
if measure(word)>width:raise ValueError('unbreakable word exceeds width')
candidate=(line+' '+word).strip()
if measure(candidate)<=width:line=candidate
else:lines.append(line);line=word
if line:lines.append(line)
if len(lines)>max_lines:raise ValueError('layout needs more lines; no silent truncation')
return lines
def baselines(top,count,leading,bottom):
positive(top);positive(leading);nonnegative(bottom)
if not isinstance(count,int) or isinstance(count,bool) or count<1 or bottom<0:raise ValueError('baseline contract')
result=[top-i*leading for i in range(count)]
if result[-1]<bottom:raise ValueError('text crosses reserved bottom')
return result
def dashes(x1,x2,y,dash=5,gap=4):
nonnegative(x1)
for v in (x2,y,dash,gap):positive(v)
if x1<0 or x2<=x1 or x2>A4[0] or y>A4[1]:raise ValueError('line outside page')
segments=[];x=x1
while x<x2:
segments.append([x,min(x+dash,x2),y]);x+=dash+gap
return segments
def make_pdf(path):
from reportlab.pdfgen.canvas import Canvas
from reportlab.pdfbase.pdfmetrics import stringWidth
c=Canvas(str(path),pagesize=A4,invariant=1);c.setAuthor('Independent synthetic module');c.setCreator('Printable geometry example');c.setProducer('Synthetic geometry renderer');c.setTitle('Words and cutting lines - synthetic worksheet')
w,h=A4;c.setFillColorRGB(.08,.22,.24);c.setFont('Helvetica-Bold',25);c.drawString(42,h-62,'Words, shapes and space')
c.setFont('Helvetica',11);c.drawString(42,h-84,'A synthetic worksheet for inspecting line layout and cutting coordinates.')
c.setStrokeColorRGB(.67,.76,.73);c.roundRect(42,310,w-84,390,12,stroke=1,fill=0)
lines=wrap('Read each instruction before you cut. Match the circle to the square, then leave the triangle in the remaining space.',w-124,lambda t:stringWidth(t,'Helvetica',14))
ys=baselines(665,len(lines),22,580);c.setFont('Helvetica',14)
for text,y in zip(lines,ys):c.drawString(62,y,text)
c.setStrokeColorRGB(.10,.36,.35);c.setLineWidth(2);c.circle(126,470,35,stroke=1,fill=0);c.rect(250,435,70,70,stroke=1,fill=0);p=c.beginPath();p.moveTo(420,510);p.lineTo(380,435);p.lineTo(460,435);p.close();c.drawPath(p,stroke=1,fill=0)
c.setFont('Helvetica',11)
for x,t in [(105,'Circle'),(267,'Square'),(400,'Triangle')]:c.drawString(x,405,t)
c.setFont('Helvetica',10);c.drawString(42,265,'CUTTING GUIDE - dashed geometry is separate from the text frame')
c.setLineWidth(.8)
for x1,x2,y in dashes(42,w-42,245):c.line(x1,y,x2,y)
c.setFont('Helvetica',10);c.drawString(42,210,'Printer scaling can change physical size. Check actual print settings separately.')
c.setFillColorRGB(.30,.37,.37);c.setFont('Helvetica',9);c.drawString(42,54,NOTICE);c.drawRightString(w-42,35,'1 / 1');c.save()
return dict(page=A4,lines=lines,baselines=ys,cutting_y=245,text_bottom=310,notice=NOTICE)긴 단어를 만나는 위치를 바꿔 본다
앞에 짧은 단어가 있어도 너무 긴 다음 단어는 여전히 폭을 넘는다. 첫 단어만 검사하는 테스트로는 놓칠 수 있어 줄이 이미 있는 경우도 검사했다. 긴 단어를 쪼개는 정책이 필요하다면 언어와 교육 목적에 맞춰 별도 정의해야 하며 지금은 무조건 잘라 넣지 않는다.
이 반례는 실행 기록과 테스트로 다시 확인할 수 있다. 다른 업무에 적용할 때는 이름만 바꾸지 말고 입력의 의미와 실패 정책이 같은지 먼저 확인해야 한다. 작은 독립 함수는 운영 프로그램 전체를 대신하지 않는다.
공개 코드와 ZIP은 같은 검토 파일을 사용한다. 실제 업체의 자료를 가져오거나 외부 시스템을 실행하지 않은 상태에서도 핵심 판단을 확인할 수 있도록 만들었다. 새로운 연결을 추가하면 그 연결의 결과는 별도로 기록해야 한다.
직접 확인한 실행과 남겨 둔 경계
소스 폴더와 ZIP을 새로 푼 폴더에서 각각 12개 테스트가 통과했다. 실행 예제의 실제 반환값도 records.json으로 남겼다. 공개 페이지의 코드는 내려받기 구현과 대조할 수 있다. 과거 프로젝트의 완료 보고와 테스트 수치를 이번 실행의 증거로 가져오지 않았다.
아래 선택기는 실제 로컬 실행에서 저장한 결과를 읽는다. Python이나 서버를 브라우저에서 실행하는 기능은 아니다. 이 검수는 실제 브라우저의 렌더링과 물리적 기기 조작을 포함하지 않는다. 실서비스 통합의 결과는 별도 근거로 확인해야 한다.
내려받아 적용하기 전에
ZIP에는 전체 코어와 테스트, 실행기, 저장된 결과와 사용 안내가 있다. 라이브 예제는 두 언어 HTML도 포함한다. 수정과 재사용이 가능한 새 합성 코드이며 고객 원본 이미지, 실데이터와 계정 정보는 없다. 실제 프로그램을 실행하는 지시와 외부 전송도 넣지 않았다.
적용할 때는 먼저 단위와 입력 출처, 실패한 경우의 안내를 실제 업무에 맞춰 정해야 한다. 테스트 통과가 모든 입력 사실과 운영 권한을 보장하지는 않는다. 필요한 인증·저장·전송과 화면 검수는 작은 계약 바깥의 일로 남긴다. 나는 재사용할 코드와 함께 어떤 판단을 다시 해야 하는지도 읽을 수 있게 공개한다.
추가 경계 검토에서 시작 좌표와 예약 하단의 NaN·무한대·참거짓을 거절하도록 보강했다. 기존 12개 테스트 메서드에 반례를 더했고 샘플 PDF와 실행 기록은 바뀌지 않았다.