Formats약 6분

PDF 텍스트 편집의 원리: 콘텐츠 스트림, 폰트 인코딩, 그리고 한 번에 한 줄만 고칠 수 있는 이유

PDF 페이지에는 문단도 텍스트 상자도 없고, 어떤 폰트로 어떤 위치에 이 글리프들을 그리라는 명령만 있습니다. 한 줄을 고치는 일은 옛 명령을 지우고 같은 자리에 새 명령을 쓰는 것이며, 무엇을 편집할 수 있고 없는지는 여기서 정해집니다.

PDF123 · Updated 2026-10-14
PDF 텍스트 편집의 원리: 콘텐츠 스트림, 폰트 인코딩, 그리고 한 번에 한 줄만 고칠 수 있는 이유

PDF 페이지에는 문단도 텍스트 상자도 없습니다. 명령의 목록이 있을 뿐입니다. 이 위치에, 이 폰트와 크기로, 이 글리프들을 그리라는 명령입니다. 텍스트를 바꾼다는 것은 그 줄을 그리는 명령을 지우고, 같은 위치에 같은 폰트, 크기, 색으로 새 명령을 쓰는 일입니다. PDF 편집이 한 번에 한 줄만 고치고 줄 바꿈을 하지 못하는 것도 그래서입니다. 텍스트가 저장되는 방식에서 그대로 따라 나오는 결과입니다.

한 줄의 텍스트가 파일 안에서 어떻게 보이는가

페이지의 내용은 콘텐츠 스트림에 들어 있습니다. 연산자로 이루어진 작은 프로그램입니다. 텍스트에서는 네 가지 연산자가 대부분의 일을 합니다. BT와 ET는 텍스트 블록을 둘러싸고, Tf는 폰트와 크기를 고르며, Td는 다음 위치로 이동하고, Tj는 문자열을 그립니다. 표준 폰트를 쓰는 가장 단순한 경우는 이렇게 생겼습니다.

BT /F1 18 Tf 20 150 Td (Hello World) Tj ET

폰트 F1을 18포인트로 고르고, x 20, y 150으로 이동해 Hello World를 그리라는 뜻입니다. Chrome으로 인쇄한 PDF는 보통 다르게 생겼습니다. 괄호 안에는 문자가 아니라 폰트의 글리프 번호가 들어가고, 위치는 Tm이 한 번에 지정합니다. 아래는 Chrome이 내보낸 페이지의 Hello World from Georgia 줄이며, 16진수 네 자리가 글리프 하나의 번호입니다.

BT /F4 24 Tf 1 0 0 -1 30 52 Tm <002b0048004f004f00520003003a00520055004f0047000300490055005200500003002a004800520055004a004c0044> Tj ET

002b는 H, 0048은 e, 004f는 l, 0003은 공백, 003a는 W이고, 이런 식으로 이어집니다. 이 번호들은 이 폰트 안에서만 의미가 있으며 Unicode가 아닙니다. 텍스트를 복사하면 뷰어가 파일의 다른 곳에 저장된 대응 표로 번호를 다시 문자로 바꿉니다.

여기서 두 가지가 따라옵니다. 첫째, 콘텐츠 스트림 어디에도 "이것은 문단이다"라고 적혀 있지 않습니다. 줄 바꿈은 위치를 다음 줄로 옮기는 또 하나의 Td나 Tm일 뿐이고, 문단이 몇 줄인지는 위치로 추정해야 합니다. 둘째, 줄에 새 문자를 쓰려면 그 폰트에서 그 문자의 번호가 필요한데, 폰트에는 파일을 쓸 때 사용된 글리프만 들어 있는 경우가 많습니다. 이것이 폰트 서브셋팅의 결과입니다.

qpdf로 스트림을 펼 수 있습니다.

qpdf --qdf --object-streams=disable in.pdf out.pdf

out.pdf를 텍스트 편집기로 열어 Tj를 검색하면 페이지에 그려지는 모든 문자열을 볼 수 있습니다. PDF에 그 밖에 무엇이 들어 있는지는 PDF 안에는 무엇이 있나를 참고하세요.

편집기가 글리프에서 "줄"을 다시 만드는 방법

편집기는 먼저 흩어진 글리프를 사용자가 클릭할 수 있는 줄로 묶어야 합니다. 폰트, 크기, 색이 같고 앞 글리프와의 간격이 폰트 크기 하나 이내인 글리프는 같은 텍스트 조각에 속합니다. 줄 바꿈, 새 폰트, 새 색이 나오면 조각이 끝납니다. 오른쪽에서 왼쪽으로 쓰는 텍스트나 세로쓰기 텍스트는 묶이지 않으며 편집용으로 열 수 없습니다.

직접 확인할 수 있는 결과가 두 가지 있습니다. 중국어 줄에 영어 단어가 끼어 있는 것처럼 두 폰트가 섞인 줄은 서로 이웃한 두 개의 텍스트 조각으로 나뉘어, 따로따로 클릭합니다. 편집 뒤에도 마찬가지입니다. 새 텍스트가 다른 폰트를 쓰면 줄이 두 텍스트 조각이 되고, 한쪽을 고쳐도 다른 쪽은 그대로입니다.

한 줄 고치기: 옛 텍스트를 지우고 새 텍스트를 쓴다

삭제에는 텍스트만 대상으로 하는 형태의 가리기를 씁니다. 줄 영역 안의 글리프가 콘텐츠 스트림에서 제거되고, 이미지와 선은 그대로 두며, 옛 텍스트 위에 색 상자도 그리지 않으므로 복사와 검색으로 더는 찾을 수 없습니다. 바뀌는 것은 페이지 내용뿐입니다. 같은 텍스트가 주석이나 문서 속성에도 있다면 그쪽은 영향을 받지 않습니다.

쓰기는 페이지 콘텐츠 스트림의 끝에 새 텍스트 명령을 덧붙입니다. 원래 줄의 기준선 위치에서 시작하고, 크기와 색을 그대로 씁니다. 위의 줄을 Hello World 2로 바꾸면 페이지 끝에 덧붙는 내용은 다음과 같습니다.

q 1 0 0 1 22.5 148.92 cm 0 0 0 rg
/Span <</ActualText <feff00480065006c006c006f00200057006f0072006c006400200032>>> BDC
BT /F4 18 Tf 0 0 Td <002b0048004f004f00520003003a00520055004f00470003> Tj ET
BT /F9 18 Tf 102.18 0 Td (2) Tj ET
EMC
Q

첫 번째 BT는 원래 폰트 F4와 원래 글리프 번호를 쓰므로 Hello World 는 원래 그대로 재사용됩니다. "2"는 원래 폰트 서브셋에 없어서, 두 번째 BT가 표준 Helvetica 폰트(F9)를 새로 시작해 그립니다. q와 Q는 이 블록의 좌표 변환과 색을 블록 안에 가둡니다. 편집기는 원래 내용도 q/Q 쌍으로 감싸므로, 원래 페이지 끝에서 짝이 맞지 않은 상태가 새 텍스트에 영향을 줄 수 없습니다. 이것은 텍스트 조각 하나의 예이며, 숫자는 파일마다 다릅니다.

끝에 덧붙이면 세 가지 결과가 생깁니다.

  • 새 텍스트는 마지막에 그려지므로 페이지의 다른 모든 것 위에 놓입니다. 원래 이미지나 색 블록 아래에 있던 텍스트가 편집 뒤에는 그 위에 나타납니다.
  • 콘텐츠 스트림 순서로 텍스트를 추출하는 도구(pdftotext -raw 등)는 편집한 줄을 그 페이지의 마지막에 읽습니다. 위치로 추출하는 방식(pdftotext -layout)에서는 변화가 보이지 않습니다. 페이지는 똑같아 보이지만 읽는 순서는 바뀌었습니다.
  • 편집기는 새 텍스트에 ActualText를 붙입니다. 글리프가 실제로 어떤 문자를 나타내는지 뷰어에 알려 주는 것입니다. 글리프 하나가 여러 문자에 대응할 수 있고, 글리프 번호만으로 거꾸로 찾으면 호환 문자가 나오기도 합니다(예를 들어 文을 강희 부수 ⽂로 읽는 경우). ActualText는 복사되는 것이 입력한 그대로가 되도록 보장합니다.

새 텍스트를 어떤 폰트로 그리는가

원래 폰트를 재사용하는 것이 가장 안전합니다. 글리프 모양, 굵기, 폭이 옆의 글자와 맞기 때문입니다. 재사용할 수 있는지는 편집기가 그 폰트에서 문자의 코드가 무엇인지 알 수 있는지에 달려 있습니다.

  • 위와 같은 복합 폰트(Type0, 2바이트 글리프 번호, Identity-H 인코딩): 폰트에서 그 문자의 글리프 번호를 찾아, 있으면 씁니다. 없으면 그 문자는 서브셋에 없는 것입니다.
  • WinAnsi 또는 MacRoman 인코딩을 쓰는 1바이트 단순 폰트(TrueType, Type1): 문자에서 바이트로의 대응이 고정되어 있고, 폭은 파일 안의 폭 표에서 가져옵니다.
  • 사용자 정의 인코딩(Differences)이나 폰트 내장 인코딩에 의존하는 폰트: 믿을 만한 대응이 없으므로 편집기는 추측하지 않고 대체 폰트로 넘어갑니다. 그러지 않으면 글자가 깨집니다.

원래 폰트에 없는 문자에는 대체 폰트를 씁니다. 순수한 라틴 문자는 모든 뷰어에 있는 표준 폰트인 Helvetica, Times, Courier 중 가장 가까운 것을 쓰므로 임베드할 것이 없습니다. 그 밖의 것, 예를 들어 한자, 가나, 한글, 태국 문자, 악센트가 있는 라틴 문자, 키릴 문자, 그리스 문자에는 오픈소스 Noto 폰트를 씁니다. 서버가 요청에 담긴 문자를 받아 오픈소스 HarfBuzz 서브셋 라이브러리로 전체 폰트에서 그 글리프만 잘라 내며, 보통 몇 KB이고, 브라우저가 이를 파일에 임베드합니다. 한자의 지역별 자형은 인터페이스 언어를 따릅니다. 중국어 간체, 중국어 번체, 일본어, 한국어 중 하나입니다.

폰트 이름만 적지 않고 글리프를 임베드하는 이유는, 이름으로만 참조한 폰트는 뷰어마다 고른 대체 폰트로 바뀌고 굵기도 뷰어마다 다를 수 있기 때문입니다. 임베드하면 모든 뷰어가 같은 글리프 모양을 보여 줍니다.

왜 한 번에 한 줄만 고칠 수 있는가

편집기에는 레이아웃 엔진이 없습니다. 파일 안의 모든 글리프 위치는 고정되어 있습니다. 새 텍스트는 폰트의 폭을 써서 시작점에서 오른쪽으로 놓일 뿐이고, "가장자리에서 줄 바꿈"하는 논리도 없으며, 그 뒤의 것을 옮기지도 않습니다. 줄을 길게 하면 늘어난 문자는 계속 오른쪽으로 이어지고, 페이지 가장자리를 넘는 부분은 보이지 않습니다. 문단을 다시 쓰거나 표에 행을 추가하려면 새 레이아웃이 필요하고, 그것은 그 PDF를 만든 원본 파일로 돌아가야 한다는 뜻입니다.

폭도 폰트가 글리프마다 정해 둔 진행 폭(글리프를 놓은 뒤 위치가 오른쪽으로 옮겨 가는 거리)뿐이며, 커닝이나 글리프 치환은 없습니다. 셰이핑(문맥에 맞는 글리프를 고르고 위치를 정하는 처리)이 필요 없는 텍스트(라틴 문자, 키릴 문자, 그리스 문자, 한자)는 올바르게 배치됩니다. 태국어 성조 부호는 제자리에 놓이려면 글리프 위치 조정이 필요한데 편집기는 이를 하지 않으므로 어긋날 수 있습니다. 아랍 문자는 문맥에 따라 달라지는 글리프 형태와 오른쪽에서 왼쪽으로의 레이아웃이 필요하고, 히브리 문자는 오른쪽에서 왼쪽으로의 레이아웃이 필요하며, 인도계 문자는 자음과 모음 부호를 합쳐 글리프로 만들어야 합니다. 이것들은 모두 완전한 텍스트 셰이핑이 필요하고 편집기가 지금은 이를 하지 않으므로, 잘못된 문자를 만들어 내는 대신 입력을 거부합니다.

파일 자체에서 오는 제한이 두 가지 더 있습니다. 스캔한 페이지의 내용은 텍스트 연산자가 없는 이미지 한 장이므로, 지우거나 바꿀 것이 없습니다. 디지털 서명이 있는 파일을 편집하면 서명이 보호하던 원래 내용이 바뀌므로 서명은 더 이상 유효하지 않습니다.

직접 확인하기

내려받은 파일에서 위에서 설명한 변화를 명령 세 개로 볼 수 있습니다.

pdftotext -raw edited.pdf -      # 콘텐츠 스트림 순서로 추출: 옛 텍스트는 사라지고, 편집한 줄이 맨 끝에 온다
pdffonts edited.pdf              # 폰트 목록: 재사용된 원래 폰트와, 새로 임베드된 폰트 서브셋
qpdf --qdf --object-streams=disable edited.pdf edited.qdf   # 콘텐츠 스트림을 펼친다: 덧붙인 BT ... ET 블록이 맨 끝에 있다

더 읽기

Open tool
Process in the browser — no watermark, files removed after the job.
Open tool