PDF에서 굵게(bold) 또는 기울임꼴(italic) 텍스트를 추출하려고 시도해 본 적이 있다면, 아마 정규 표현식(regex)부터 시작했을 것입니다. 그것이 가장 당연한 방법처럼 느껴지기 때문입니다. 폰트 이름에서 "Bold"라는 단어를 찾아 텍스트에 표시를 하고 다음으로 넘어가는 식이죠. 이 전략은 잠시 동안은 잘 작동하는 것처럼 보여 잘못된 자신감을 심어줍니다. 그러다 누군가 Acrobat의 다른 버전이나 LibreOffice, 혹은 print-to-PDF 드라이버를 통해 동일한 문서를 내보내면, 당신이 코드로 작성한 모든 가정이 무너져 버립니다.
폰트 이름이 거짓말을 하는 이유
PDF.js는 ABCDEF+TimesNewRomanPS-BoldMT와 같은 문자열을 전달합니다. 앞의 6글자는 내보내기 과정에서 삽입된 무작위 접두사이며, 파일이 다시 생성될 때마다 변경됩니다. 파서를 이 접두사에 의존하는 것은 노이즈에 도박을 거는 것과 같습니다. 다른 내보내기 도구들은 훨씬 더 도움이 되지 않습니다. 어떤 것들은 Font12나 F1 같은 단순한 식별자만 내보내기도 합니다. 이러한 레이블은 아무런 의미론적(semantic) 의미를 담고 있지 않습니다. 파일이 작성될 당시 근처에 있던 내부 리소스 태그일 뿐입니다.
PDF(Portable Document Format)는 후속 텍스트 추출을 쉽게 하도록 설계되지 않았기 때문에, 폰트 이름은 단순히 임베디드(embedded)되거나 서브셋(subsetted)된 리소스를 가리키는 참조일 뿐입니다. 스타일 감지를 위한 안정적인 API 역할을 하도록 의도된 적이 없습니다. Bold나 Italic이라는 하위 문자열을 찾는 정규 표현식을 작성하는 것은, 제작 애플리케이션이 원하는 대로 형식을 지정할 수 있는 레이블을 긁어모으는 것과 같습니다. 당신은 실제 타이포그래피 속성을 읽는 것이 아닙니다. 파일 명명 규칙을 읽고 있는 것이며, 규칙은 계약이 아닙니다.
레이블이 아닌 디스크립터를 읽으세요
진실은 다른 곳에 있습니다. PDF.js에서 모든 페이지 객체는 페이지를 렌더링하는 데 필요한 실제 폰트 디스크립터(descriptor)를 보유한 맵인 commonObjs를 노출합니다. 라이브러리가 페이지를 파싱할 때, 이 맵을 실제 폰트 객체로 채웁니다. 해당 객체들은 bold와 italic에 대한 불리언(boolean) 속성을 노출합니다. 이 불리언 값들은 이름 문자열로부터 추론된 것이 아닙니다. PDF 내부에 임베디드된 폰트 디스크립터에서 유래하며, 글리프 메트릭(glyph metrics), OS/2 테이블 플래그, 그리고 타이포그래퍼가 문서에 작성한 심볼릭 디스크립터로부터 파생됩니다.
즉, 더 이상 추측할 필요가 없다는 뜻입니다. 페이지의 텍스트 항목을 순회하기 전에, page.commonObjs를 반복하여 fontStyleMap을 만드세요. 각 폰트 ID에 대해, 폰트 객체가 제공하는 실제 .bold 및 .italic 속성을 기록하는 객체를 저장합니다. 나중에 각 텍스트 항목을 처리할 때, 맵에서 해당 폰트 참조를 찾아 미리 계산된 플래그를 읽으면 됩니다. 그러면 내보내기 방식에 관계없이 변하지 않는 결정론적인(deterministic) 답을 얻을 수 있습니다.
폴백(fallback)을 유지할 수도 있습니다. 만약 디스크립터가 누락되었거나 불완전하다면, 폰트 이름에서 접두사를 제거하고 무작위 태그를 버린 뒤 남은 부분에 대해 보수적인 정규 표현식을 실행하세요. 하지만 이것은 주 로직이 아니라 최후의 수단이어야 합니다. 신뢰성의 차이는 극적입니다. 이름 기반 파싱이 내보내기 도구마다 깨지는 반면, 디스크립터 기반 파싱은 파일이 실제로 무엇을 포함하고 있는지 묻기 때문에 안정적으로 유지됩니다.
폰트마저 거짓말을 할 때: 합성 스타일
디스크립터조차 놓치는 경우가 있습니다. 일부 PDF 제작 도구는 별도의 이탤릭체 서체를 임베디드하는 번거로움을 피합니다. 대신, 수직 형태의 로만(roman) 폰트를 가져와 변환 행렬(transform matrix)로 기울입니다. 이는 타이포그래피의 순수성보다 파일 크기를 우선시하는 디자인 도구나 오래된 워드 프로세서로 생성된 파일에서 흔히 볼 수 있습니다.
PDF.js의 모든 텍스트 항목은 글리프 좌표계를 페이지 좌표계로 매핑하는 6개 요소의 아핀 행렬(affine matrix)인 transform 배열을 가집니다. 해당 배열의 세 번째 요소가 수평 전단(horizontal shear)을 제어합니다. 이 값이 0이 아니면, 텍스트는 렌더러에 의해 기계적으로 기울어진 상태입니다. 폰트 디스크립터만 신뢰한다면 이 텍스트를 일반 로만체로 분류하겠지만, 행렬을 검사하면 합성된 이탤릭체를 포착하여 올바르게 표시할 수 있습니다. 오버프린팅(overprinting)으로 생성된 합성 볼드체에도 동일한 논리가 적용되지만, 기하학적 구조만으로 이를 감지하는 것은 더 어렵습니다. 기울어진 텍스트의 경우, 전단(shear) 값이 결정적인 증거(smoking gun)가 됩니다.
밑줄은 선언되는 것이 아니라 그려지는 것입니다
볼드와 이탤릭은 폰트 속성입니다. 밑줄은 그렇지 않습니다. PDF에서 밑줄은 벡터 경로(vector path)입니다. 렌더러는 텍스트 베이스라인 근처에 위치한 얇은 수평 세그먼트에 대한 그리기 명령을 내립니다. 이는 cmap에 저장된 문자 속성이 아니라, 글리프 아래에 위치하는 그래픽 요소일 뿐입니다.
이러한 구분이 중요한 이유는 아무리 폰트 디스크립터를 읽어도 밑줄을 찾아낼 수 없기 때문입니다. 페이지의 원시 그리기 연산자(drawing operators)를 확인하거나, 짧은 수평 선분이 베이스라인과 적절한 근접성을 유지하며 평행하게 지나가는지 기하학적 수준(geometry-level)의 출력을 살펴봐야 합니다. 추출 엔진이 텍스트 런(text run) 아래에서 이러한 선분을 발견하면, 해당 런을 밑줄이 있는 것으로 태깅합니다. 이를 별도의 탐지 레이어로 처리하면 데이터 모델의 정직성을 유지할 수 있습니다. 굵게(bold)와 이탤릭(italic)은 폰트에 내재된 속성이지만, 밑줄은 문서에 의해 렌더링되는 외재적인 장식이기 때문입니다.
작동하는 파이프라인
깔끔한 추출 시스템은 관심사를 별도의 레이어로 분리합니다. 먼저, geometry worker가 페이지를 탐색합니다. 이 워커는 page.commonObjs를 쿼리하여 fontStyleMap을 구성하고, 각 텍스트 항목의 transform array를 검사하여 합성 이탤릭체(synthetic italics)를 잡아내며, 주변의 벡터 경로(vector paths)를 스캔하여 밑줄을 찾아냅니다. 그 결과물은 textMeta라고 부르는 깔끔한 중간 구조체이며, 여기서 모든 텍스트 런은 bold, italic, underline이라는 세 가지 단순한 불리언(boolean) 값을 가집니다.
다음으로, 텍스트 리빌더(text rebuilder)가 해당 구조를 소비하여 마크업된 출력을 생성합니다. 여기서 중첩 순서가 중요합니다. 올바른 계층 구조는 밑줄을 가장 바깥쪽에, 그다음 이탤릭, 그리고 가장 안쪽에 굵게를 배치하는 것입니다. 즉, 스타일이 완전히 적용된 런은 <u><i><b>text</b></i></u>가 됩니다. 이러한 순서는 잘못된 HTML 중첩을 방지하고 브라우저와 문서 변환기 전반에서 일관된 렌더링을 유지합니다. 또한 이는 타이포그래피 논리를 반영합니다. 즉, 장식은 의미론적 강조를 감싸고, 의미론적 강조는 구조적 무게감을 감쌉니다.
이 접근 방식의 강력함은 PDF가 이미 스스로에 대해 알고 있는 정보만을 사용한다는 점에 있습니다. OCR도 필요 없고, 클라우드 비전 서비스도 필요 없으며, 래스터화된 픽셀로부터 스타일을 추측하는 머신러닝 모델도 필요하지 않습니다. 여러분은 제작 애플리케이션이 이미 계산해 놓은 기하학 및 메타데이터를 통해 노출된 파일 자체의 의미론적 레이어를 읽고 있는 것입니다. 그 결과는 빠르고, 결정론적이며, 혼란스러운 PDF 생성기 환경 전반에서 정확합니다.
핵심 요점
폰트 이름을 대상으로 하는 정규 표현식(Regex)은 함정입니다. 테스트한 파일 하나에서는 잘 작동하기 때문에 지름길처럼 느껴지겠지만, 두 번째 내보내기 작업에서 발생하는 약간의 압력에도 무너지고 맙니다. 진짜 정보는 이미 PDF 내부에 디스크립터, 행렬(matrices), 벡터 경로에 자리 잡고 있습니다. 이러한 사실을 바탕으로 파이프라인을 구축하십시오. 폰트 객체에 굵은 글씨인지 물어보고, shear(기울임)를 확인하기 위해 transform matrix를 체크하십시오. 밑줄을 찾기 위해 그려진 선분들을 살펴보십시오. 문서의 표면적인 라벨을 긁어모으는 대신 문서 자체의 엔지니어링을 쿼리한다면, 하나의 PDF 생성기에서 다른 생성기로 넘어가더라도 유지되는 스타일을 얻을 수 있습니다.
