HotPDF Delphi Component는 v2.768.15부터 가짜 볼드와 겹쳐 인쇄된 로고가 추출된 PDF 텍스트에 남기는 중복 문자를 제거합니다. 생산자가 같은 글리프를 몇 푸인트의 일부 차이로 두 번 그리면, ExtractLoadedPageText, ExtractLoadedPageTextLayout, ExtractLoadedPageTextColumns는 이제 그 문자를 한 번만 읽습니다. "Bold words"가 더 이상 "Bold wordsBold words"로 나오지 않습니다
이것은 디코더 실패처럼 보이지만 아닌 버그 중 하나입니다. 모든 글리프는 올바르게 디코드됩니다; 텍스트는 정말로 페이지에 두 번 있습니다. 수정은 두 개의 완전히 유효한 글리프 중 어느 쪽이 복사본인지 판정해야 하고, "fill"의 ll이나 표의 동일한 셀들 같은 정당한 반복 글자를 먹어 치우지 않고 그러해야 합니다. 이 글의 나머지는 HotPDF가 쓰는 규칙, 그 이면의 임계값, 다른 추출 도구와 일부러 다르게 간 지점, 그리고 같은 릴리스 열차에서 온 또 하나의 무관한 추출 버그, 행 끝을 잘라 버리던 그 버그를 설명합니다
가짜 볼드는 PDF 추출에서 왜 텍스트를 두 배로 만들까요?
가짜 볼드가 추출 텍스트를 두 배로 만드는 이유는 생산자가 문자열을 두 번 그리고 양쪽 다 실제 텍스트 표시 연산이기 때문입니다. 폰트 패밀리에 볼드 페이스가 없거나 저작 도구가 그냥 귀찮아할 때, 많은 생성기는 같은 문자열을 Tj나 TJ로 표시하고, 텍스트 위치를 푸인트의 몇 1/10만큼 오른쪽으로 밀고, 다시 표시하는 방식으로 볼드를 흉내 냅니다. 화면에서는 두 복사본이 더 두꺼운 스트로크로 합쳐집니다. 콘텐츠 스트림에는 문자 코드의 완전한 런이 두 개 있고, ISO 32000-1 §9.4는 추출기에게 "이쪽이 장식"이라고 알려 주는 플래그를 하나도 주지 않습니다
겹쳐 인쇄된 로고는 다른 각도에서 같은 문제를 일으킵니다. 로고타입은 같은 폰트와 같은 문자로 채우기로 한 번, 스트로크나 약간 밀린 그림자로 또 한 번 그려질 수 있습니다. 콘텐츠 스트림을 그리기 순서대로 훑는 순진한 추출기는 첫 런 전체를 내보낸 다음 두 번째 런 전체를 내보내고, 그래서 결과가 "BBoolldd"가 아니라 "Bold wordsBold words"로 읽히는 것입니다. v2.768.15 이전 HotPDF 텍스트 조립은 양쪽 복사본을 모두 갖고 있었고, 중복은 호출자가 실제로 가장 신경 쓰는 것을 망가뜨렸습니다: 두 런 사이의 단어 경계가 사라져서 페이지 텍스트 전체에서 "Bold words" 검색이 실패할 수 있었습니다
진짜 텍스트 렌더링 모드 2(채우기 후 스트로크, ISO 32000-1 §9.3.6)에는 이 문제가 없습니다. 하나의 텍스트 표시 연산이 한 세트의 글리프를 만들기 때문입니다. 중복은 생산자가 텍스트를 두 번 발행할 때만 나타납니다
HotPDF는 어느 글리프가 중복인지 어떻게 판정하나요?
HotPDF는 두 글리프가 같은 Unicode 값을 가지고, 쓰기 방향이 같고, 크기가 서로 5% 이내이며, 거의 겹쳐 놓여 있을 때 — 기준선을 따라서는 글리프 크기의 0.1 이내, 기준선을 가로질러서는 0.2 이내 — 하나의 그려진 문자로 취급합니다. 그런 쌍마다 먼저 그려진 글리프를 남기고 나중 것을 버립니다. v2.768.15 구현의 값들이며, 각각 다 이유가 있습니다
- 같은 Unicode, 같은 방향. 동일한 문자만이 중복일 수 있고, 가로 글리프는 세로(WMode 1) 글리프와 절대 비교되지 않습니다
- 크기 5% 이내. 우연히 겹친 큰 헤딩 글자와 작은 캡션 글자는 같은 텍스트가 아니므로, 크기 불일치는 쌍을 후보에서 뺍니다
- 기준선 방향으로 크기의 0.1. 가짜 볼드 오프셋은 푸인트의 몇 1/10, 일반 텍스트 크기의 몇 퍼센트입니다. 진짜 인접 글자 둘은 꽉 조인 커닝의 "ll"조차 대략 어드밴스 폭 하나만큼 떨어져 있어, 이 허용 오차보다 한 자릿수 큽니다
- 기준선 가로 방향으로 크기의 0.2. 그림자와 아웃라인 패스는 오른쪽과 동시에 아래로도 밀리는 일이 잦으므로 세로 허용 오차가 더 느슨하지만, 다음 줄까지의 거리보다는 여전히 훨씬 작습니다
- 먼저 그려진 쪽이 이김. 앞선 글리프를 남기면 첫 런의 읽기 순서가 보존되므로, 살아남은 텍스트는 여전히 생산자가 쓴 대로 흐릅니다
거리는 날 페이지 X와 Y가 아니라 글리프 자신의 기준선 프레임에서 잡히므로, 회전된 텍스트와 기울어진 기준선의 텍스트도 일반 가로 줄과 같은 허용 오차를 씁니다. 쓰이는 크기는 Tf에 주어진 크기 오퍼랜드가 아니라 쿼드에서 유도된, 페이지에 그려진 대로의 글리프 크기입니다; Tf를 1로 두고 텍스트 행렬로 모든 것을 스케일하는 문서에서는 날 폰트 크기가 모든 허용 오차를 무용지물로 만드니까요
공백은 결코 후보가 아닙니다. U+0020까지의 코드 포인트, 논브레이크 스페이스 U+00A0, 전각 스페이스 U+3000은 건너뜁니다. 빈 글리프에는 중복될 보이는 잉크가 없고, 하나를 버리면 단어가 합쳐지기 때문입니다
촘촘한 페이지에서 검사를 빠르게 유지하기
HotPDF의 중복 검사는 대략 n log n 시간에 돌고, n 제곱이 아닙니다. 정렬을 먼저 하고 작은 창 안의 글리프만 비교하기 때문입니다. 모든 글리프를 서로 비교하는 것은 정확하지만 쓸 수 없습니다: 작은 활자의 촘촘한 페이지는 글리프를 쉽게 수만 개씩 안고, 검사는 텍스트 추출 호출마다 돌아갑니다
내부 루틴은 안정적 병합 정렬로 공백이 아닌 글리프들을 Unicode 값, 그다음 원점 X, 그다음 그리기 순서로 정렬합니다. 정렬 후에는 글리프의 잠재적 중복이 전부 그 바로 뒤에 놓이므로, 스캔은 Unicode 값이 일치하고 X 차이가 글리프 크기의 0.25 안에 있는 동안만 앞으로 걸어 갑니다. 0.25 창은 기준선 방향 허용 오차 0.1보다 일부러 넓어서, 약간 기울어진 기준선 위의 쌍도 정밀한 기준선 프레임 검사가 돌기 전에 창 안에 들어옵니다. 미묘한 경우 하나: 글리프 A에서 시작한 스캔이 A보다 먼저 그려진 일치 글리프를 만나면 A가 복사본이므로 A를 버리고 스캔을 멈춰서, 앞선 글리프가 더 뒤쪽과의 비교를 이어 갑니다. 이 규칙 덕에 "먼저 그려진 쪽이 이김"이 쌍은 물론 삼중 타격에서도 성립합니다
비교는 폰트나 문자 코드가 아니라 Unicode 값으로 이뤄집니다. 서로 다른 폰트를 통해 같은 Unicode로 매핑되는 두 글리프, 예컨대 한 임베디드 서브셋의 채우기 패스와 다른 서브셋의 스트로크 패스도 여전히 하나의 문자로 인식됩니다. 뒷면은, 코드가 Unicode 0으로 해석되는 글리프에는 신뢰할 만한 비교 대상이 없다는 것이고, 이는 HotPDF로 로드된 PDF에서 텍스트 추출하기에서 다루는 미해결 카운트에 신경 쓸 또 하나의 이유입니다
어느 HotPDF 추출 호출이 중복 글리프를 제거하나요?
HotPDF의 텍스트를 만드는 호출은 모두 중복을 제거하고, 날 글리프 호출은 그러지 않습니다. 중복 제거는 페이지의 가시 박스 밖 글리프를 버리는 단계이기도 한 THotPDF.ExtractVisibleLoadedPageGlyphs에 살고, 일반 텍스트, 칼럼, 레이아웃, 시맨틱 텍스트 추출은 모두 여기서 글리프를 읽습니다. THotPDF.ExtractLoadedPageGlyphs는 겹쳐 인쇄를 포함해 콘텐츠 스트림이 그리는 모든 글리프를 여전히 반환합니다. 하이라이팅, 레닥션, 포렌식 도구를 만드는 호출자는 그려진 것의 완전한 기록이 필요하기 때문입니다
평범한 텍스트 작업에서는 호출 코드가 달라지는 것이 없습니다. v2.768.15 이전에 두 배 텍스트를 뽑아 내던 같은 루프가 이제는 각 단어를 한 번씩 뽑습니다
var
Pdf: THotPDF;
PageCount, I: Integer;
PageText: UnicodeString;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('brochure.pdf');
for I := 0 to PageCount - 1 do
if Pdf.ExtractLoadedPageText(I, PageText) then
if Pos('Bold words', PageText) > 0 then
Writeln('Found on page ', I + 1);
finally
Pdf.Free;
end;
end;
레이아웃과 칼럼 출력도 마찬가지로 이득을 봅니다. 중복된 런은 문자 그리드 위에서 줄의 나머지를 옆으로 밀어 내곤 했습니다. ExtractLoadedPageTextColumns는 선택적 갭 배수를 그대로 유지합니다; 중복 필터는 칼럼 분할 전에 돌므로 그 설정과 상호작용하지 않습니다
var
Grid, Columns: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1-layout.txt', Grid, TEncoding.UTF8);
// 간격이 중앙 글리프 어드밴스의 3배를 넘는 곳에서 칼럼을 분할
if Pdf.ExtractLoadedPageTextColumns(0, Columns, 3.0) then
TFile.WriteAllText('page1-columns.txt', Columns, TEncoding.UTF8);
end;
한 페이지에 겹쳐 인쇄된 텍스트가 아예 있는지 알고 싶다면 두 글리프 호출을 비교하세요. 그 차이는 크롭 박스 밖으로 나간 글리프도 셉니다. 따라서 정확한 수치가 아니라 중복의 상한으로 취급하세요
var
AllGlyphs, Visible: THPDFGlyphArray;
begin
if Pdf.ExtractLoadedPageGlyphs(0, AllGlyphs) and
Pdf.ExtractVisibleLoadedPageGlyphs(0, Visible) then
Writeln(Format('%d painted, %d read as text (%d overprinted or off-page)',
[Length(AllGlyphs), Length(Visible),
Length(AllGlyphs) - Length(Visible)]));
end;
이 글리프들을 텍스트로 바꾸는 단어 간격과 줄바꿈 규칙은 필터의 영향을 받지 않습니다; HotPDF가 페이지 텍스트에 단어 공백과 줄바꿈을 놓는 방식에서 자세히 다룹니다
이 규칙이 다른 도구와 일부러 다르게 가는 지점
HotPDF는 일부 다른 추출 도구가 남겨 두는 완전 중복을 버리며, 그 차이는 의도적입니다. v2.768.15 변경을 레퍼런스 코퍼스로 검증할 때, 정확히 같은 위치에 두 번 그려진 페이지 푸터가 HotPDF에서는 한 번, 레퍼런스 추출기에서는 두 번 추출되었습니다. 동일한 글리프 위에 그려진 글리프는 별개의 문자로는 보이지 않으므로, 한 번 읽는 것이 페이지가 실제로 보여 주는 것입니다
정직한 한계는 반대 방향으로도 있습니다. 두 번째 패스를 기준선 방향으로 글리프 크기의 0.1보다 크게 밀어 놓은 가짜 볼드는 잡히지 않고, 눈에 띄게 다른 크기로 그린 그림자 패스도 그렇습니다. 이런 레이아웃은 실전에서 드뭅니다. 오프셋이 그만큼 커지면 시각 효과가 볼드처럼 보이지 않게 되니까요. 하지만 장식적인 헤딩에서 두 배 텍스트가 보이면 필터가 망가졌다고 단정하기 전에 ExtractLoadedPageGlyphs의 글리프 X 값을 비교해 보세요. 필터는 또한 기하학적으로 같은 자리에 있지 않은 중복에는 아무것도 하지 않습니다. 페이지의 다른 곳에 숨겨진 레이어로 반복되는 러닝 헤더 같은 것 말입니다; 그건 콘텐츠 문제이지 겹쳐 인쇄가 아닙니다
CID 폰트 텍스트에서 줄 끝이 잘렸던 이유
HotPDF v2.768.12에서 고쳐진 별개의 버그는 CID 키드(Type 0) 폰트, 그러니까 /W 폭 배열이 CID 순으로 정렬되지 않은 폰트로 짜인 텍스트의 줄 끝을 떨어뜨렸습니다. ISO 32000-1 §9.7.4.3은 /W 배열 형식을 정의하지만 항목들이 CID 오름차순일 것을 요구하지 않으며, 일부 생성기는 글리프가 문서에서 처음 쓰인 순서대로 씁니다
텍스트 추출 디코더는 이진 탐색으로 폭을 찾았고, 이진 탐색은 정렬된 입력을 조용히 가정합니다. 정렬되지 않은 배열에서는 대부분의 탐색이 빗나가 기본 폭 /DW, 즉 1000 단위, 풀 em으로 폴백했습니다. 그런 폰트의 라틴 텍스트는 모든 글자가 전각인 것처럼 배치되고, 줄은 페이지 오른쪽 끝을 한참 넘어 흘렀으며, 가시 박스 밖에 놓인 글리프는 버려졌습니다. 그래서 줄 끝이 추출 텍스트에서도 검색에서도 사라졌습니다. 아무것도 크래시하지 않고 아무것도 보고되지 않았고, 그래서 발견이 어려웠습니다
HotPDF는 이제 /W 항목들을 읽자마자 안정적 병합 정렬로 정렬하므로, 두 항목이 겹치는 곳에서는 먼저 쓰인 쪽이 여전히 이기고, 이는 배열을 파일 순서로 읽었을 때와 일치합니다. 페이지 렌더러는 한 번도 영향받지 않았습니다. 폭 데이터의 자체 사본을 들고 선형으로 훑기 때문이고, 세로 폭 배열 /W2도 선형으로 훑습니다. 페이지는 완벽해 보이는데 추출만 엉망일 때 이 분업을 기억해 둘 가치가 있습니다: PDF 라이브러리에서 렌더링과 추출은 생각보다 훨씬 적은 코드를 공유하곤 합니다
FAQ
HotPDF는 "fill"의 ll 같은 반복 글자를 제거하나요?
아니요. 진짜 인접 글자 둘은 대략 어드밴스 폭 하나만큼 떨어져 있는 반면, HotPDF는 기준선 방향으로 글리프 크기의 0.1 이내, 가로 방향으로 0.2 이내인 동일 문자만 병합하므로, 정당한 반복은 남습니다
겹쳐 인쇄된 중복을 포함해 모든 글리프를 얻으려면 어떻게 하나요?
THotPDF.ExtractLoadedPageGlyphs를 호출하세요. 페이지가 그리는 모든 글리프를 그리기 순서대로 반환합니다. THotPDF.ExtractVisibleLoadedPageGlyphs와 모든 텍스트 추출 호출은 중복 제거된 가시 집합을 반환합니다
가짜 볼드의 두 배 텍스트를 고친 HotPDF 버전은 무엇인가요?
HotPDF v2.768.15가 텍스트, 레이아웃, 칼럼, 시맨틱 텍스트 추출에 중복 글리프 제거를 더했고, v2.768.12가 정렬되지 않은 /W 배열을 가진 CID 폰트의 사라진 줄 끝을 고쳤습니다
두 수정 모두 현재 HotPDF Delphi PDF 컴포넌트에 들어 있습니다. 이 컴포넌트는 PDF 생성, 편집, 렌더링 기능과 함께 Delphi와 C++Builder에서 로드된 PDF의 텍스트를 추출합니다