성능
PDF Oxide v0.3.11은 3,830개 PDF 코퍼스에서 Python 텍스트 추출 평균 0.8ms를 달성합니다. PyMuPDF보다 5배, pypdf보다 15배 빠르며, 유효한 PDF에 대해 100% 통과율을 기록합니다.
벤치마크 결과
코퍼스: 3,830개 PDF
세 개의 독립적인 공개 테스트 스위트를 결합했습니다:
| 스위트 | PDF 수 | 출처 |
|---|---|---|
| veraPDF | 2,907 | PDF/A 적합성 테스트 코퍼스 |
| Mozilla pdf.js | 897 | 브라우저 PDF 렌더링 테스트 스위트 |
| SafeDocs | 26 | DARPA SafeDocs 비정상 PDF 코퍼스 |
이 코퍼스가 신뢰할 수 있는 이유
이 PDF들은 임의로 선별된 것이 아닙니다. 각 스위트는 표준 기관이나 주요 오픈소스 프로젝트가 관리하는 검증된 테스트 코퍼스입니다:
-
veraPDF는 PDF 표준 커뮤니티에서 10년 이상 사용해 온 공식 PDF/A 적합성 검증 도구입니다. 2,907개의 테스트 파일은 각각 하나의 PDF 사양 기능을 정확히 테스트하는 원자적 구성으로, 모든 PDF/A 버전(1A/1B, 2A/2B/2U, 3A/3B/3U, 4/4E/4F), PDF/UA 접근성(UA1, UA2), ISO 32000-1 (PDF 1.7)과 ISO 32000-2 (PDF 2.0)를 모두 포함합니다. CC BY 4.0 라이선스입니다.
-
Mozilla pdf.js는 Firefox에서 PDF 렌더링을 담당하며, 매년 수십억 개의 PDF를 처리합니다. 897개의 테스트 파일은 복잡한 다중 페이지 문서, 주석 유형(테두리 스타일, 하이라이트, 캐럿, 파일 첨부), 폼 위젯, 비정상적인 폰트 인코딩, 대용량 스트레스 테스트 문서, 콘텐츠 스트림 엣지 케이스 등 실제 렌더링 엣지 케이스를 다룹니다. 7개 파일은 올바른 오류 거부를 테스트하기 위해 의도적으로 손상되었습니다.
-
DARPA SafeDocs는 파서 견고성에 초점을 맞춘 미국 정부 지원 보안 연구 프로그램입니다. 26개 파일은 가장 어려운 엣지 케이스를 타겟팅합니다: Type3 폰트의 콘텐츠 스트림 순환, 이중 startxref 트레일러, 압축된 PDF 구문, 방언 변형, 인라인 이미지 엣지 케이스, 재귀적 폰트 중첩, 유니코드 비밀번호(UTF-16 LE 포함)를 사용한 암호화된 PDF 등입니다. 이러한 파일들은 취약한 파서를 충돌시키거나, 멈추게 하거나, 악용하도록 설계되었습니다.
이 코퍼스가 다루는 범위
- 모든 주요 PDF 버전: PDF 1.0부터 PDF 2.0까지
- 암호화 및 비밀번호: AES-256, RC4, 유니코드 비밀번호, UTF-16 LE 인코딩
- 보안 엣지 케이스: 재귀 구조, 콘텐츠 스트림 순환, 비정상 트레일러, 퍼징 생성 손상
- 폰트 다양성: TrueType, CIDFont, Type1, Type3, CJK 인코딩, 임베디드 서브셋
- 문서 복잡도: 단일 페이지 픽스처부터 10,000+ 페이지 문서, 인라인 이미지, 중첩된 Form XObject
- 올바른 거부: 의도적으로 손상된 7개 파일(누락된 PDF 헤더, 잘못된 xref 스트림) – 이러한 파일을 "파싱"하는 라이브러리는 덜 안전한 것이지, 더 신뢰할 수 있는 것이 아닙니다
3,830개 파일 중 3,823개가 유효한 PDF입니다. 7개의 유효하지 않은 파일은 에러 처리를 위한 테스트 픽스처이며, PDF Oxide는 7개 모두를 올바르게 거부합니다.
Python 라이브러리 비교
전체 3,830개 PDF 코퍼스에서의 PDF당 평균 텍스트 추출 시간:
| 라이브러리 | 평균 | p99 | 통과율 | 라이선스 |
|---|---|---|---|---|
| PDF Oxide | 0.8ms | 9ms | 100% | MIT |
| PyMuPDF | 4.6ms | 28ms | 99.3% | AGPL-3.0 |
| pypdfium2 | 4.1ms | 42ms | 99.2% | Apache-2.0 |
| pymupdf4llm | 55.5ms | 280ms | 99.1% | AGPL-3.0 |
| pdftext | 7.3ms | 82ms | 99.0% | GPL-3.0 |
| pdfminer | 16.8ms | 124ms | 98.8% | MIT |
| pdfplumber | 23.2ms | 189ms | 98.8% | MIT |
| markitdown | 108.8ms | 378ms | 98.6% | MIT |
| pypdf | 12.1ms | 97ms | 98.4% | BSD-3 |
PDF Oxide는 현재 사용 가능한 가장 빠른 Python PDF 라이브러리입니다. PyMuPDF와 달리 MIT 라이선스를 사용하므로, 상용 소프트웨어에서도 AGPL 제약 없이 사용할 수 있습니다.
Rust 라이브러리 비교
| 라이브러리 | 평균 | p99 | 통과율 | 텍스트 추출 |
|---|---|---|---|---|
| PDF Oxide | 0.8ms | 9ms | 100% | 내장, 프로덕션 수준 |
| oxidize_pdf | 13.5ms | 11ms | 99.1% | 기본 |
| unpdf | 2.8ms | 10ms | 95.1% | 기본 |
| pdf_extract | 4.08ms | 37ms | 91.5% | 기본 |
| lopdf | 0.3ms | 2ms | 80.2% | 내장 추출 기능 없음 |
lopdf는 파싱할 수 있는 PDF에서는 더 빠르지만, 코퍼스의 20%에서 실패합니다. pdf_oxide는 100% 신뢰성과 내장 텍스트 추출을 결합한 유일한 Rust 크레이트입니다. lopdf는 텍스트 추출 기능을 제공하지 않으므로, 폰트 디코딩과 간격 분석을 직접 구현해야 합니다.
텍스트 품질
PDF Oxide는 전체 코퍼스에서 PyMuPDF 및 pypdfium2와 비교하여 99.5%의 텍스트 동등성을 달성합니다. 품질은 3,823개의 유효한 PDF 전체에서 추출된 텍스트 출력을 문자 단위로 비교하여 측정했습니다.
코퍼스별 분석
| 코퍼스 | PDF 수 | PDF Oxide 평균 | pypdfium2 평균 | PyMuPDF 평균 |
|---|---|---|---|---|
| veraPDF | 2,907 | 0.7ms | 3.6ms | 4.1ms |
| Mozilla pdf.js | 897 | 1.1ms | 5.8ms | 6.2ms |
| SafeDocs | 26 | 0.9ms | 4.0ms | 4.3ms |
최적화 이력: v0.3.5 -> v0.3.8
v0.3.8에서는 두 가지 핵심적인 O(n) 병목 현상을 제거하여 동일한 코퍼스에서 평균이 23.3ms에서 0.8ms(Python)로 감소했습니다.
1. 페이지 트리 일괄 캐시
이전: get_page()는 캐시되지 않은 모든 페이지에 대해 루트부터 페이지 트리를 순회했습니다. 전체 페이지를 순차적으로 추출할 경우, 페이지당 O(n), 전체 O(n^2)이었습니다.
이후: 첫 번째 페이지 접근 시 전체 페이지 트리를 한 번 순회하고, 모든 페이지를 HashMap<usize, Object>에 캐시합니다. 이후 모든 접근은 O(1)입니다.
이 수정으로 10,000페이지 PDF가 55초에서 332밀리초로 단축되었습니다.
2. 객체 오프셋 스캔 캐시
이전: xref 테이블에 객체가 없을 경우, scan_for_object()는 누락된 각 객체에 대해 전체 PDF 파일을 읽었습니다. 수백 개의 구조 트리 요소가 xref에 없는 태그된 PDF에서 수백 번의 전체 파일 읽기가 발생했습니다.
이후: 파일을 한 번 스캔하고 모든 객체 오프셋을 HashMap에 캐시합니다. 이후 조회는 O(1)입니다.
3. 단일 패스 텍스트 추출
이전: extract_spans()는 페이지 콘텐츠에 대해 두 번의 패스를 실행했습니다. 첫 번째는 문서 유형(학술 논문, 신문, 양식 등)을 분류하고, 두 번째는 텍스트를 추출했습니다.
이후: 분류 패스를 완전히 제거했습니다. 적응형 폰트 인식 임계값이 단일 패스에서 동등하거나 더 나은 결과를 생성합니다.
4. 콘텐츠 스트림 사전 할당
이전: parse_content_stream()은 기본 용량부터 시작하여 연산자 Vec을 구축했으며, 대용량 콘텐츠 스트림에서 반복적인 재할당이 발생했습니다.
이후: 스트림 크기를 기반으로 Vec를 사전 할당합니다(data.len() / 20). 이는 20바이트당 대략 하나의 연산자를 추정한 것입니다.
방법론
모든 벤치마크는 동일한 방법론을 사용합니다:
- 각 라이브러리는 Python multiprocessing을 사용하여 모든 3,830개 PDF를 처리합니다 (프로세스당 하나의 PDF)
- PDF당 60초 타임아웃 – 이를 초과하는 PDF는 실패로 처리합니다
- 추출된 텍스트는 품질 비교를 위해 라이브러리별로 디스크에 저장합니다
- 파일 열기부터 최종 텍스트 추출까지의 벽시계 시간을 측정합니다
- 워밍업 실행 없음, 파일 간 캐싱 없음
- PDF당 단일 스레드
벤치마크 하네스는 동일한 머신, 동일한 코퍼스, 동일한 조건에서 모든 18개 라이브러리(Rust 3개, Python 15개)를 실행합니다.
벤치마크 재현
공개 테스트 코퍼스는 자유롭게 이용 가능합니다:
- veraPDF: github.com/veraPDF/veraPDF-corpus
- Mozilla pdf.js: github.com/mozilla/pdf.js/tree/master/test/pdfs
- SafeDocs: github.com/pdf-association/safedocs
검증 실행:
cargo run --release --example verify_corpus -- \
/path/to/veraPDF-corpus \
/path/to/pdfjs-test \
/path/to/safedocs \
--csv results.csv
성능 특성
PDF Oxide가 빠른 영역
- 텍스트 추출: 주요 최적화 대상입니다. 일반적인 문서에서 서브 밀리초 성능을 제공합니다.
- 순차적 다중 페이지 추출: 페이지 트리 캐시 덕분에 대용량 문서에서 전체 페이지를 추출하는 것이 단일 페이지 추출과 거의 같은 속도입니다.
- 태그된 PDF: 구조 트리 순회와 객체 해석이 캐시됩니다.
- 비정상 PDF: 폴백 전략을 사용한 관대한 파싱으로 비용이 큰 재시도를 방지합니다.
선형으로 확장되는 영역
- 페이지 수: 각 페이지는 독립적으로 처리됩니다. 100페이지는 대략 1페이지의 100배가 소요됩니다.
- 콘텐츠 스트림 크기: 연산자 파싱은 스트림 길이에 선형적입니다.
- 이미지 추출: 이미지의 수와 크기에 비례합니다.
느린 결과를 예상해야 하는 경우
- OCR이 포함된 스캔 PDF: OCR 처리(활성화된 경우)는 텍스트 추출보다 현저히 느립니다.
- 렌더링: 페이지를 이미지로 렌더링하는 것은 콘텐츠 복잡도와 목표 DPI에 따라 달라집니다.
- 강력하게 암호화된 PDF: AES-256 복호화는 스트림당 오버헤드를 추가합니다.
- 수천 개의 폰트가 포함된 PDF: 폰트 파싱은 문서당 캐시되지만, 초기 파싱은 폰트 수에 비례합니다.
다음 단계
- 변경 이력 – 전체 버전 이력
- Python 라이브러리 비교 – PyMuPDF, pypdf, pdfplumber, pdfminer와의 상세 비교
- Rust 라이브러리 비교 – lopdf, pdf_extract, pdf-rs와의 상세 비교
- Rust 시작하기 – 설치 및 첫 번째 추출
- Rust API 레퍼런스 – 전체 API 문서