티스토리 뷰


안녕하세요, 이파피루스입니다.
앞으로 이파피루스의 신제품 데이터 추출 파이썬 라이브러리 PyMuPDF Pro의 유용하고도 다양한 활용법을 실제 코드 예시를 포함하여 시리즈로 전해드릴 예정입니다. 
많은 관심 부탁드립니다😊 

추출된 데이터가 부족하게 느껴지거나 문서 전체의 내용이 잘 추출되지 않았던 경험,
혹은 문서 처리 시간이 너무 길어 파이프라인이 불필요하게 지연된 경험이 있으신가요?

이파피루스에서 텍스트 추출의 두 가지 주요 접근 방식인 ‘네이티브(Native)’와 ‘OCR’을 소개하고, 이를 어떻게 그리고 언제 활용하면 효율적일지 소개합니다!


1. 네이티브 텍스트 추출이란?

이 방식은 PyMuPDF Pro의 핵심 기능을 이용해 문서에서 텍스트를 직접 추출하는 방법입니다. Page.get_text() 메서드를 사용하여 PDF 내에서 “텍스트”로 식별된 실제 콘텐츠를 추출합니다.

  • 정의: PDF에 디지털 방식으로 내장된 텍스트를 추출
  • 작동 방식: PDF 구조 내의 텍스트 객체에 직접 접근
  • 장점:
    • 매우 빠른 처리 속도
    • (텍스트가 존재할 경우) 완벽한 정확도
    • 원본 서식과 글꼴을 그대로 유지
    • 낮은 연산 자원 소모
  • 한계:
    • 디지털 방식으로 생성된 PDF에서만 작동
    • 스캔된 문서에서는 전혀 작동하지 않음
    • 복잡한 레이아웃에서는 문제가 발생할 수 있음

2. OCR (광학 문자 인식) 이란?

이 방법은 오픈소스 3rd 파티 기술인 Tesseract를 활용하여 페이지의 이미지를 스캔하고, 이를 텍스트로 변환하는 방식입니다. 예를 들어, 정보의 스크린샷을 포함하는 PDF는 단순히 “이미지”로 인식되지만, 우리는 '읽을 수 있는 텍스트(machine-readable text)'가 필요합니다.  PyMuPDF Pro의 Page.get_textpage_ocr() 기능을 활용하여 이러한 작업을 수행합니다.

  • 정의: 텍스트가 포함된 이미지를 읽을 수 있는(machine-readable text)텍스트로 변환
  • 작동 방식: 이미지 처리 및 패턴 인식
  • 장점:
    • 모든 유형의 PDF에 적용 가능 (스캔본, 사진, 이미지 기반)
    • 필기체 텍스트도 처리 가능 (고급 모델 사용 시)
    • 네이티브 방식이 놓치는 시각적 요소도 처리 가능
  • 한계:
    • 느린 처리 속도
    • 이미지 품질에 따라 정확도 달라짐
    • 높은 연산 및 메모리 요구
    • 인식 오류 발생 가능성 있음

네이티브 텍스트 추출을 사용해야하는 경우

네이티브 방식은 속도와 대량 문서 처리가 필요할 때 가장 적합합니다.  
실제 업무 환경에서는 많은 문서가 스캔된 문서이거나, 의도적으로 페이지를 이미지 형식으로 “플랫(flatten)”하거나 “베이크(bake)”한 PDF입니다.

  • 대표 사례:
    • 디지털 방식으로 생성된 문서 (예: Word에서 내보낸 문서, 자동 생성된 보고서)
    • 처리 속도가 중요한 대량 문서 작업
    • 완벽한 정확도가 요구되는 경우
    • 구조가 명확한 비즈니스 문서
  • 네이티브 방식이 작동하지 않는 경우:
    • 스캔된 문서
    • 사진으로 생성된 PDF
    • 텍스트가 포함된 이미지가 삽입된 문서

 

  • 코드샘플
1
2
3
4
5
6
7
8
9
import pymupdf
 
doc = pymupdf.open("a.pdf"# open a document
out = open("output.txt""wb"# create a text output
for page in doc: # iterate the document pages
    text = page.get_text().encode("utf8"# get plain text (is in UTF-8)
    out.write(text) # write text of page
    out.write(bytes((12,))) # write page delimiter (form feed 0x0C)
out.close()
cs

 

OCR을 사용해야 할 때

문서가 스캔된 것이라는 사실을 알고 있다면, 선택의 여지 없이 OCR을 사용해야 합니다. 텍스트 콘텐츠를 추출하려면 OCR에 의존할 수밖에 없습니다.

  • 대표 사례:
    • 스캔된 문서나 팩스
    • 사진을 기반으로 생성된 PDF
    • 오래된 역사적 문서
    • 네이티브 텍스트와 텍스트가 포함된 이미지가 혼합된 콘텐츠
    • PDF 내 이미지에서 텍스트를 추출해야 할 때
  • 품질 관련 고려사항:
    • 해상도 요건 (최소 300 DPI 권장)
    • 원본 자료의 상태 (깨끗한 vs. 손상된 문서)
    • 언어 및 글꼴 인식 가능성
    • 코드샘플
1
2
3
4
5
6
7
8
9
import pymupdf
 
doc = pymupdf.open("a.pdf"# open a document
 
for page in doc: # iterate the document pages
    textPage = page.get_textpage_ocr()
    # analyse the text page as required!
 
out.close()
cs

 


✨하이브리드 접근법: 두 가지 방식의 장점을 모두 활용하기

다음은 PDF 데이터 추출의 효율을 극대화하기 위한 가이드라인입니다.

추출 방식 제안:

데이터 추출을 보다 견고하게 만들기 위해 먼저 네이티브 텍스트 추출을 시도하고, 실패할 경우 OCR을 적용하는 것을 권장합니다.
예를 들어, 문서에서 특정 필드를 찾고자 할 때 네이티브 방식으로 찾을 수 없다면 해당 문서를 다시 PyMuPDF Pro로 넘겨 OCR을 수행하는 식입니다.

탐지 방법

대량의 문서를 처리해야 한다면 문서 유형에 따라 필터링하여 서로 다른 파이프라인에서 처리하는 것이 좋습니다.
예를 들어 PDF에서 이미지 개수를 계산해 이미지가 많은 문서라고 판단되면 OCR 파이프라인으로 바로 보내는 방식입니다.
다른 탐지 기준으로는 파일 크기를 들 수 있으며, 크기가 작은 PDF는 이미지가 거의 없을 가능성이 높습니다.

OCR 파이프라인을 고려해야 하는 특별한 경우:

  • 페이지 전체가 이미지로 덮여 있음
  • 페이지 내 텍스트 객체가 전혀 없음
  • 수천 개의 작은 벡터 그래픽이 존재 (시뮬레이션된 텍스트일 가능성)

구현 워크플로우

Step 1: 문서 유형 필터링

  • Set 1: 이미지가 거의 없는 소형 PDF
  • Set 2: 이미지가 많은 대형 PDF 또는 완전히 스캔된 “베이크(baked)”된 PDF (OCR이 유리하다고 판단된 문서)

Step 2: Set 1에 대해 네이티브 추출 시도

Step 3: 결과 평가

  • 추출된 텍스트가 비어 있거나, 깨져 있거나, 불완전할 경우 해당 문서를 Set 2로 이동

Step 4: Set 2 문서에 대해 선별적으로 OCR 적용


PyMuPDF Pro와 하이브리드 처리

PyMuPDF Pro의 API는 이와 같은 하이브리드 접근을 지원합니다.
이상적으로는 전체 문서 페이지를 분석하여 OCR이 필요한 페이지와 그렇지 않은 페이지를 구분하는 것이 바람직합니다.
예를 들어, 100페이지짜리 문서가 OCR 대상이라고 판단되었더라도, 실제로는 그중 30페이지만 OCR이 필요할 수 있습니다.
이런 경우 선택적으로 OCR이 필요한 페이지만 추려내는 것이 연산 비용을 줄이는 데 효과적입니다.

이후에 제공되는 예제 코드는 PyMuPDF Pro를 활용하여 PDF를 분석하고 큰 이미지가 감지된 페이지 요약 정보를 반환합니다.

    • 코드샘플
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
import pymupdf  # PyMuPDF
import os
from typing import List, Dict, Tuple
 
def analyze_images_in_pdf(pdf_path: str, size_threshold_mb: float = 1.0
                         dimension_threshold: Tuple[intint= (800600)) -> Dict:
    """
    Analyze a PDF document for large images on each page.
    
    Args:
        pdf_path (str): Path to the PDF file
        size_threshold_mb (float): Minimum file size in MB to consider an image "large"
        dimension_threshold (tuple): Minimum (width, height) to consider an image "large"
    
    Returns:
        dict: Analysis results containing image information for each page
    """
    
    try:
        doc = pymupdf.open(pdf_path)
        total_pages = len(doc)
        
        print(f"Analyzing {total_pages} pages in: {os.path.basename(pdf_path)}")
        print(f"Size threshold: {size_threshold_mb} MB")
        print(f"Dimension threshold: {dimension_threshold[0]}x{dimension_threshold[1]} pixels")
        print("-" * 60)
        
        results = {
            'pdf_path': pdf_path,
            'total_pages': total_pages,
            'size_threshold_mb': size_threshold_mb,
            'dimension_threshold': dimension_threshold,
            'pages_with_large_images': [],
            'summary': {
                'images'0,
                'total_large_images'0,
                'pages_with_large_images'0,
                'total_image_size_mb'0,
                'largest_image'None
            }
        }
        
        largest_image_size = 0
        
        # Analyze each page (limit to 100 pages as requested)
        pages_to_analyze = min(total_pages, 100)
        
        for page_num in range(pages_to_analyze):
            page = doc[page_num]
            page_info = {
                'page_number': page_num + 1,
                'images': [],
                'large_images': [],
                'total_images_on_page'0,
                'large_images_count'0
            }
            
            # Get all images on the page
            image_list = page.get_images()
            page_info['total_images_on_page'= len(image_list)
            
            for img_index, img in enumerate(image_list):
                try:
                    # Extract image information
                    xref = img[0]  # xref number
                    pix = pymupdf.Pixmap(doc, xref)
                    
                    # Skip if image has alpha channel and convert if needed
                    if pix.alpha:
                        pix = pymupdf.Pixmap(pymupdf.csRGB, pix)
                    
                    # Get image properties
                    width = pix.width
                    height = pix.height
                    image_size_bytes = len(pix.tobytes())
                    image_size_mb = image_size_bytes / (1024 * 1024)
 
                    print(f"Found image with size:{image_size_bytes} bytes")
                    
                    # Check if image meets "large" criteria
                    is_large_by_size = image_size_mb >= size_threshold_mb
                    is_large_by_dimensions = (width >= dimension_threshold[0and 
                                            height >= dimension_threshold[1])
                    
                    if is_large_by_size or is_large_by_dimensions:
                        image_info = {
                            'image_index': img_index + 1,
                            'xref': xref,
                            'width': width,
                            'height': height,
                            'size_mb': round(image_size_mb, 2),
                            'size_bytes': image_size_bytes,
                            'colorspace': pix.colorspace.name if pix.colorspace else 'Unknown',
                            'reason_large': []
                        }
                        
                        if is_large_by_size:
                            image_info['reason_large'].append('Size')
                        if is_large_by_dimensions:
                            image_info['reason_large'].append('Dimensions')
                        
                        page_info['large_images'].append(image_info)
                        page_info['large_images_count'+= 1
                        results['summary']['total_large_images'+= 1
                        results['summary']['total_image_size_mb'+= image_size_mb
                        
                        # Track largest image
                        if image_size_mb > largest_image_size:
                            largest_image_size = image_size_mb
                            results['summary']['largest_image'= {
                                'page': page_num + 1,
                                'size_mb': round(image_size_mb, 2),
                                'dimensions': f"{width}x{height}",
                                'xref': xref
                            }
                    
                    results['summary']['images'+= 1
                    pix = None  # Clean up
                    
                except Exception as e:
                    print(f"Error processing image {img_index + 1} on page {page_num + 1}: {e}")
                    continue
            
            # Only add pages that have large images
            if page_info['large_images_count'> 0:
                results['pages_with_large_images'].append(page_info)
                results['summary']['pages_with_large_images'+= 1
            
            # Progress indicator
            if (page_num + 1) % 10 == 0:
                print(f"Processed {page_num + 1} pages...")
        
        doc.close()
        results['summary']['total_image_size_mb'= round(results['summary']['total_image_size_mb'], 2)
        
        return results
        
    except Exception as e:
        print(f"Error analyzing PDF: {e}")
        return None
 
def print_analysis_results(results: Dict):
    """Print formatted analysis results."""
    
    if not results:
        print("No results to display.")
        return
    
    print("\n" + "="*60)
    print("PDF IMAGE ANALYSIS RESULTS")
    print("="*60)
    
    # Summary
    summary = results['summary']
    print(f"Total pages analyzed: {results['total_pages']}")
    print(f"Total images: {summary['images']}")
    print(f"Pages with large images: {summary['pages_with_large_images']}")
    print(f"Total large images found: {summary['total_large_images']}")
    print(f"Total size of large images: {summary['total_image_size_mb']} MB")
    
    if summary['largest_image']:
        largest = summary['largest_image']
        print(f"Largest image: {largest['size_mb']} MB ({largest['dimensions']}) on page {largest['page']}")
    
    print("\n" + "-"*60)
    print("DETAILED RESULTS BY PAGE")
    print("-"*60)
    
    # Detailed results
    for page_info in results['pages_with_large_images']:
        print(f"\nPage {page_info['page_number']}:")
        print(f"  Total images on page: {page_info['total_images_on_page']}")
        print(f"  Large images: {page_info['large_images_count']}")
        
        for img in page_info['large_images']:
            reasons = ", ".join(img['reason_large'])
            print(f"    Image {img['image_index']}: {img['width']}x{img['height']} pixels, "
                  f"{img['size_mb']} MB ({reasons})")
 
def save_analysis_to_file(results: Dict, output_file: str):
    """Save analysis results to a text file."""
    
    if not results:
        print("No results to save.")
        return
    
    with open(output_file, 'w'as f:
        f.write("PDF IMAGE ANALYSIS RESULTS\n")
        f.write("="*60 + "\n")
        f.write(f"PDF File: {results['pdf_path']}\n")
        f.write(f"Analysis Date: {pymupdf.Document().metadata.get('creationDate', 'Unknown')}\n")
        f.write(f"Size Threshold: {results['size_threshold_mb']} MB\n")
        f.write(f"Dimension Threshold: {results['dimension_threshold'][0]}x{results['dimension_threshold'][1]}\n\n")
        
        # Summary
        summary = results['summary']
        f.write("SUMMARY\n")
        f.write("-"*20 + "\n")
        f.write(f"Total pages analyzed: {results['total_pages']}\n")
        f.write(f"Pages with large images: {summary['pages_with_large_images']}\n")
        f.write(f"Total large images: {summary['total_large_images']}\n")
        f.write(f"Total size of large images: {summary['total_image_size_mb']} MB\n")
        
        if summary['largest_image']:
            largest = summary['largest_image']
            f.write(f"Largest image: {largest['size_mb']} MB ({largest['dimensions']}) on page {largest['page']}\n")
        
        # Detailed results
        f.write("\nDETAILED RESULTS\n")
        f.write("-"*20 + "\n")
        
        for page_info in results['pages_with_large_images']:
            f.write(f"\nPage {page_info['page_number']}:\n")
            f.write(f"  Total images: {page_info['total_images_on_page']}\n")
            f.write(f"  Large images: {page_info['large_images_count']}\n")
            
            for img in page_info['large_images']:
                reasons = ", ".join(img['reason_large'])
                f.write(f"    Image {img['image_index']}: {img['width']}x{img['height']} px, "
                        f"{img['size_mb']} MB, {img['colorspace']} ({reasons})\n")
    
    print(f"Analysis results saved to: {output_file}")
 
# Example usage
if __name__ == "__main__":
    # Replace with your PDF file path
    pdf_file = "test.pdf"
    
    # Customize thresholds as needed
    size_threshold = 1.0  # MB
    dimension_threshold = (800600)  # width x height pixels
    
    # Run analysis
    results = analyze_images_in_pdf(
        pdf_path=pdf_file,
        size_threshold_mb=size_threshold,
        dimension_threshold=dimension_threshold
    )
 
    if results:
        # Print results to console
        print_analysis_results(results)
 
        # Optionally save to file
        output_file = f"image_analysis_{os.path.splitext(os.path.basename(pdf_file))[0]}.txt"
        save_analysis_to_file(results, output_file)
    else:
        print("Analysis failed. Please check your PDF file path and try again.")
cs

 

실제 업무 환경에서는 이렇게 사용하세요

텍스트 추출 전략에서 문서 필터링을 활용하면 시간과 처리 자원을 절약할 수 있습니다.
예를 들어, 다양한 형식의 문서를 다루는 금융 서비스 업계나, 대부분 디지털화된 과거 사건 기록을 보유한 법률 사무소에서는 특히 효과적입니다.
또한 많은 학술 논문에는 주요 데이터를 담은 시각화 그래픽이 포함되어 있는 경우가 많습니다.
이러한 모든 사례에서 필요한 페이지만 선택적으로 OCR을 적용하는 스마트 전략이 합리적입니다.


문서화 및 동영상 자료 (Documentation & Videos)

아래는 이파피루스의 미국 자회사인 아티펙스(Artifex)에서 제작한 동일한 내용의 영문 영상입니다. 자세한 튜토리얼이 필요하실 경우 활용해주세요 🫡

YouTube: Advanced PyMuPDF Text Extraction Techniques | Full Tutorial


앞으로, 다양하고 유익한 PyMuPDF Pro 튜토리얼로 찾아오겠습니다. 기대해주세요! 감사합니다 :)